From 3158875270b4450761596c463df8487d931bac10 Mon Sep 17 00:00:00 2001 From: feiyu Date: Thu, 27 Aug 2026 17:32:29 +0700 Subject: [PATCH 01/21] ant colony: materialise ANN backlog in a walker sidecar A walk holds the score-engine lock for tens of seconds while a checkpoint window is ~21s, so it runs in a process re-exec'd with --ant-walk-worker. Also releases ANN claims a fork child inherits, which the on-demand waiter would otherwise spin on forever. --- CMakeLists.txt | 1 + src/CMakeLists.txt | 4 + src/extensions/ant_colony_maintenance.h | 47 + src/extensions/ant_walker_client.h | 944 ++++++++++++++++++ src/extensions/ant_walker_proto.h | 134 +++ src/extensions/ant_walker_worker.h | 603 +++++++++++ .../http/controller/rpc_stats_controller.h | 10 + src/extensions/supervisor_shim.h | 63 +- src/platform/concurrency.h | 38 +- src/qubic.cpp | 54 +- test/ant_colony.cpp | 102 ++ 11 files changed, 1987 insertions(+), 13 deletions(-) create mode 100644 src/extensions/ant_colony_maintenance.h create mode 100644 src/extensions/ant_walker_client.h create mode 100644 src/extensions/ant_walker_proto.h create mode 100644 src/extensions/ant_walker_worker.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f640e996..b89b96ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,6 +61,7 @@ option(BUILD_BENCHMARK "Build the EFI benchmark application" OFF) option(BUILD_BINARY "Build the EFI application" ON) option(USE_SANITIZER "Build test with sanitizer support (clang only)" ON) option(LITE_WASM_SC "Enable testnet-lite-RAM Wasm smart contracts" OFF) +option(ANT_WALKER "Build the ant-colony walker sidecar and its node client" ON) set(QUBIC_STANDALONE_RELEASE ON CACHE INTERNAL "Standalone builds are the only supported configuration" FORCE) set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build static dependencies" FORCE) set(OPENSSL_USE_STATIC_LIBS TRUE CACHE BOOL "Use static OpenSSL" FORCE) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index eb4cb27c..7cf11416 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -395,6 +395,10 @@ if(IS_CLANG) endif() target_link_libraries(qubic-cli PRIVATE m) +if(ANT_WALKER) + target_compile_definitions(Qubic PRIVATE ANT_WALKER) +endif() + # Configure linker settings based on compiler if(IS_MSVC) if(NOT LITE_WASM_SC) diff --git a/src/extensions/ant_colony_maintenance.h b/src/extensions/ant_colony_maintenance.h new file mode 100644 index 00000000..60c2867b --- /dev/null +++ b/src/extensions/ant_colony_maintenance.h @@ -0,0 +1,47 @@ +#pragma once + +// Colony upkeep the node does outside consensus: dropping claims a fork child inherited, and +// deciding which records a background rebuild may take next. Both are pure functions of a colony so +// they can be exercised without a running node. + +namespace AntColonyMaintenance +{ +// An ant record claimed for a network rebuild sits at ANT_ANN_MATERIALISING until the claiming thread +// publishes or releases it. fork() clones only the calling thread, so a promoted child can inherit a +// claim whose owner never existed there, and ensureAntRecordAnn's waiter would spin on it forever. +inline unsigned int releaseInheritedClaims(AntColonyBpp9000T& colony) +{ + unsigned int released = 0; + const unsigned int recordCount = colony.solutionCount(); + for (unsigned int index = 0; index < recordCount; index++) + { + if (colony.isAnnClaimHeld(index)) + { + colony.releaseAnnClaim(index); + released++; + } + } + return released; +} + +// A record can be rebuilt only once its parent holds a network, so a scan in commit order - which is +// topological - walks each lineage from the bottom up and never repeats a level. +inline bool isRebuildableNow(AntColonyBpp9000T& colony, unsigned int index) +{ + if (colony.isAnnMaterialised(index) || colony.isAnnClaimHeld(index)) + { + return false; + } + const AntSolutionRecord* record = colony.recordAt(index); + if (record == nullptr) + { + return false; + } + if (record->parentRef.isRoot()) + { + return true; + } + const long long parentIndex = colony.findIndexBySolutionRef(record->parentRef); + return parentIndex != ANT_INVALID_INDEX && colony.isAnnMaterialised((unsigned int)parentIndex); +} +} diff --git a/src/extensions/ant_walker_client.h b/src/extensions/ant_walker_client.h new file mode 100644 index 00000000..fd1db01a --- /dev/null +++ b/src/extensions/ant_walker_client.h @@ -0,0 +1,944 @@ +#pragma once + +// Node side of the ant walker sidecar: picks records whose network was never built, hands the walk +// to a separate process, and publishes the result after re-verifying it against the record. +// +// An AUX node trusting claimed scores commits every ant record unmaterialised, so the backlog grows +// all epoch and is paid back at ~one full walk per lineage level the first time a strict path needs +// a network (epoch end, a rollback replay, a MAIN switch). The walk cannot run on a node thread: it +// holds a score-engine lock for tens of seconds while a checkpoint window is only ~21 s, so the fork +// census would skip nearly every fork and force those ticks strict. Out of process the node holds no +// lock while the walk runs. + +#if defined(ANT_WALKER) && !defined(_WIN32) + +#include "ant_walker_proto.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace AntWalker +{ +enum class LinkState +{ + Disabled, + Disconnected, + Connecting, + Handshaking, + Ready, +}; + +// A walker that scores differently than this node fails every job. Marking each one would poison +// good records permanently, so a run of them is read as a broken walker instead: the marks are +// rolled back and the link is dropped. +static constexpr unsigned int DISAGREEMENT_STREAK_LIMIT = 3; +static constexpr unsigned int DEADLINE_STREAK_LIMIT = 3; +// A walk runs for minutes, and the walker answers PING throughout, so this is the "gone, not slow" +// threshold rather than an expected duration. +static constexpr long long MIN_JOB_DEADLINE_MS = 600'000; +static constexpr long long PING_INTERVAL_MS = 10'000; +static constexpr long long BACKOFF_MIN_MS = 100; +static constexpr long long BACKOFF_MAX_MS = 30'000; +static constexpr int POLL_SLICE_MS = 100; +static constexpr long long HEARTBEAT_INTERVAL_MS = 60'000; +// Generous enough for a legitimate 512 MB pool derive, short enough that a wedged walker is visible. +static constexpr long long HANDSHAKE_DEADLINE_MS = 120'000; + +struct InFlight +{ + unsigned long long jobId; + unsigned int recordIndex; + long long sentAtMs; + // Kept so the replay-cache key can be rebuilt when the result lands; re-resolving the anchor then + // could pick a different digest once the ring has moved on. + m256i anchorDigest; +}; + +struct State +{ + std::string socketPath; + unsigned int threadCount = 0; + bool debug = false; + + std::atomic link{ (int)LinkState::Disabled }; + std::atomic fd{ -1 }; + std::atomic stopping{ false }; + std::atomic quiesceRequested{ false }; + std::atomic quiesceAcknowledged{ true }; + std::atomic seedGeneration{ 0 }; + std::atomic walkerPid{ -1 }; + std::atomic suspectWalkerPid{ -1 }; + + std::atomic jobsSent{ 0 }; + std::atomic materialised{ 0 }; + std::atomic memoHits{ 0 }; + std::atomic disagreements{ 0 }; + std::atomic deadlineExpiries{ 0 }; + std::atomic staleDropped{ 0 }; + std::atomic reconnects{ 0 }; + std::atomic failedCount{ 0 }; + std::atomic walkMsEma{ 0 }; + std::atomic lastResultAtMs{ 0 }; + std::atomic inFlightCount{ 0 }; + std::atomic backlog{ 0 }; + + std::vector failedBits; + std::vector rolledBackCandidates; + std::vector inFlight; + unsigned long long nextJobId = 1; + unsigned int cursor = 0; + unsigned int disagreementStreak = 0; + unsigned int deadlineStreak = 0; + unsigned int helloGeneration = 0; + long long handshakeStartedAtMs = 0; + + std::thread* dispatcher = nullptr; +}; + +inline State gState; + +inline const char* linkName(LinkState state) +{ + switch (state) + { + case LinkState::Disabled: return "disabled"; + case LinkState::Disconnected: return "disconnected"; + case LinkState::Connecting: return "connecting"; + case LinkState::Handshaking: return "handshaking"; + case LinkState::Ready: return "ready"; + } + return "?"; +} + +inline long long nowMs() +{ + return std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); +} + +inline void logLine(const char* format, ...) +{ + va_list arguments; + va_start(arguments, format); + fprintf(stderr, "[ant-walk] "); + vfprintf(stderr, format, arguments); + fprintf(stderr, "\n"); + va_end(arguments); + fflush(stderr); +} + +inline bool isEnabled() +{ + return gState.threadCount > 0 && !gState.socketPath.empty(); +} + +// ── failed-record bitmap ──────────────────────────────────────────────────────────────────────── +// Node-local scheduling state only: it decides what the background dispatcher tries next. The +// on-demand rebuild path ignores it and still walks, because that runs when consensus needs the +// network rather than when spare capacity allows it. + +inline void ensureFailedBits() +{ + const size_t needed = (size_t)ANT_MAX_NODES_PER_EPOCH / 8; + if (gState.failedBits.size() != needed) + { + gState.failedBits.assign(needed, 0); + } +} + +inline bool isFailed(unsigned int index) +{ + ensureFailedBits(); + return (gState.failedBits[index >> 3] >> (index & 7)) & 1; +} + +inline void markFailed(unsigned int index) +{ + ensureFailedBits(); + if (!isFailed(index)) + { + gState.failedCount.fetch_add(1, std::memory_order_relaxed); + } + gState.failedBits[index >> 3] |= (unsigned char)(1u << (index & 7)); +} + +inline void clearFailed(unsigned int index) +{ + ensureFailedBits(); + if (isFailed(index)) + { + gState.failedCount.fetch_sub(1, std::memory_order_relaxed); + } + gState.failedBits[index >> 3] &= (unsigned char)~(1u << (index & 7)); +} + +// ── framing ───────────────────────────────────────────────────────────────────────────────────── + +inline bool writeFully(int fd, const void* buffer, size_t size) +{ + const unsigned char* in = (const unsigned char*)buffer; + size_t done = 0; + while (done < size) + { + const ssize_t put = write(fd, in + done, size - done); + if (put > 0) + { + done += (size_t)put; + continue; + } + if (put < 0 && errno == EINTR) + { + continue; + } + return false; + } + return true; +} + +inline bool readFully(int fd, void* buffer, size_t size) +{ + unsigned char* out = (unsigned char*)buffer; + size_t done = 0; + while (done < size) + { + const ssize_t got = read(fd, out + done, size - done); + if (got > 0) + { + done += (size_t)got; + continue; + } + if (got < 0 && errno == EINTR) + { + continue; + } + return false; + } + return true; +} + +inline bool sendFrame(int fd, unsigned int type, const void* payload, unsigned int payloadSize) +{ + AntWalkProto::FrameHeader header; + header.magic = AntWalkProto::MAGIC; + header.type = type; + header.payloadSize = payloadSize; + header.reserved = 0; + if (!writeFully(fd, &header, sizeof(header))) + { + return false; + } + return payloadSize == 0 || writeFully(fd, payload, payloadSize); +} + +// ── link ──────────────────────────────────────────────────────────────────────────────────────── + +// Every exit from Ready goes through here, so no claim is ever left held behind a connection that no +// longer exists - the on-demand rebuild path would otherwise wait on it forever. +inline void dropLink(const char* reason) +{ + const int fd = gState.fd.exchange(-1); + if (fd >= 0) + { + close(fd); + } + unsigned int released = 0; + for (const InFlight& job : gState.inFlight) + { + gAntColony.releaseAnnClaim(job.recordIndex); + released++; + } + gState.inFlight.clear(); + gState.inFlightCount.store(0, std::memory_order_release); + if (gState.link.load(std::memory_order_acquire) != (int)LinkState::Disconnected) + { + logLine("sidecar lost (%s), %u claims released", reason, released); + } + gState.link.store((int)LinkState::Disconnected, std::memory_order_release); +} + +inline bool sendHello(int fd) +{ + AntWalkProto::HelloPayload hello; + memset(&hello, 0, sizeof(hello)); + hello.version = AntWalkProto::VERSION; + hello.epochId = gState.seedGeneration.load(std::memory_order_acquire); + hello.annBytes = AntWalkProto::ANN_BYTES; + hello.configHash = AntWalkProto::CONFIG_HASH; + gState.helloGeneration = hello.epochId; + copyMem(hello.miningSeed, score->currentRandomSeed.m256i_u8, 32); + copyMem(hello.topologyHash, BPP9000_TOPOLOGY_HASH, 32); + copyMem(hello.dataHash, BPP9000_DATA_HASH, 32); + return sendFrame(fd, AntWalkProto::MsgHello, &hello, (unsigned int)sizeof(hello)); +} + +inline bool tryConnect() +{ + gState.link.store((int)LinkState::Connecting, std::memory_order_release); + const int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) + { + return false; + } + struct sockaddr_un address; + memset(&address, 0, sizeof(address)); + address.sun_family = AF_UNIX; + snprintf(address.sun_path, sizeof(address.sun_path), "%s", gState.socketPath.c_str()); + if (connect(fd, (struct sockaddr*)&address, sizeof(address)) != 0) + { + close(fd); + gState.link.store((int)LinkState::Disconnected, std::memory_order_release); + return false; + } + + gState.fd.store(fd, std::memory_order_release); + gState.link.store((int)LinkState::Handshaking, std::memory_order_release); + gState.handshakeStartedAtMs = nowMs(); + if (!sendHello(fd)) + { + dropLink("hello write failed"); + return false; + } + return true; +} + +// ── selection ─────────────────────────────────────────────────────────────────────────────────── + +// Commit order is topological, so a forward scan that only takes records whose parent already has a +// network walks each lineage from the bottom up and never repeats a level. +inline bool selectNextRecord(unsigned int& outIndex) +{ + const unsigned int recordCount = gAntColony.solutionCount(); + if (recordCount == 0) + { + return false; + } + + for (unsigned int scanned = 0; scanned < recordCount; scanned++) + { + const unsigned int index = (gState.cursor + scanned) % recordCount; + if (isFailed(index) || !AntColonyMaintenance::isRebuildableNow(gAntColony, index)) + { + continue; + } + gState.cursor = (index + 1) % recordCount; + outIndex = index; + return true; + } + return false; +} + +// Counting the whole backlog is a full pass over the records, so it runs on the heartbeat rather +// than on every dispatch: at mainnet's 2^23 records that scan is not something to repeat per job. +inline void refreshBacklog() +{ + const unsigned int recordCount = gAntColony.solutionCount(); + unsigned int backlogCount = 0; + for (unsigned int index = 0; index < recordCount; index++) + { + if (!gAntColony.isAnnMaterialised(index)) + { + backlogCount++; + } + } + gState.backlog.store(backlogCount, std::memory_order_release); +} + +inline bool dispatchOne() +{ + unsigned int index; + if (!selectNextRecord(index)) + { + return false; + } + if (gAntColony.tryClaimAnn(index) != AntColonyBpp9000T::AnnClaimOwned) + { + return false; + } + const AntSolutionRecord* record = gAntColony.recordAt(index); + if (record == nullptr) + { + gAntColony.releaseAnnClaim(index); + return false; + } + + AntWalkProto::JobPayload job; + memset(&job, 0, sizeof(job)); + job.jobId = gState.nextJobId++; + job.epochId = gState.seedGeneration.load(std::memory_order_acquire); + job.isRoot = record->parentRef.isRoot() ? 1u : 0u; + copyMem(job.pubkey, record->pubkey.m256i_u8, 32); + copyMem(job.nonce, record->nonce.m256i_u8, 32); + + m256i anchorDigest; + if (!getAntAnchorDigestForRebuild(record->anchorTick, anchorDigest)) + { + gAntColony.releaseAnnClaim(index); + markFailed(index); + return false; + } + copyMem(job.anchorDigest, anchorDigest.m256i_u8, 32); + + if (!job.isRoot) + { + const long long parentIndex = gAntColony.findIndexBySolutionRef(record->parentRef); + const AntSolutionRecord* parentRecord = + (parentIndex == ANT_INVALID_INDEX) ? nullptr : gAntColony.recordAt(parentIndex); + AntColonyBpp9000T::Ann parentAnn; + if (parentRecord == nullptr || !gAntColony.annOfNonRoot(*parentRecord, parentAnn)) + { + gAntColony.releaseAnnClaim(index); + return false; + } + copyMem(job.parentAnn, &parentAnn, sizeof(parentAnn)); + } + + const int fd = gState.fd.load(std::memory_order_acquire); + if (fd < 0 || !sendFrame(fd, AntWalkProto::MsgJob, &job, (unsigned int)sizeof(job))) + { + gAntColony.releaseAnnClaim(index); + dropLink("job write failed"); + return false; + } + + gState.inFlight.push_back(InFlight{ job.jobId, index, nowMs(), anchorDigest }); + gState.inFlightCount.store((unsigned int)gState.inFlight.size(), std::memory_order_release); + gState.jobsSent.fetch_add(1, std::memory_order_relaxed); + if (gState.debug) + { + logLine("job %llu record %u depth %u sent", (unsigned long long)job.jobId, index, + (unsigned)record->depth); + } + return true; +} + +// ── results ───────────────────────────────────────────────────────────────────────────────────── + +inline bool takeInFlight(unsigned long long jobId, InFlight& out) +{ + for (size_t i = 0; i < gState.inFlight.size(); i++) + { + if (gState.inFlight[i].jobId == jobId) + { + out = gState.inFlight[i]; + gState.inFlight.erase(gState.inFlight.begin() + (long)i); + gState.inFlightCount.store((unsigned int)gState.inFlight.size(), std::memory_order_release); + return true; + } + } + return false; +} + +// A walker that scores this node's records wrongly fails all of them, so a streak is treated as a +// broken walker rather than broken records: the marks it caused are undone and the link is dropped. +inline void noteDisagreement(unsigned int index) +{ + markFailed(index); + gState.rolledBackCandidates.push_back(index); + gState.disagreements.fetch_add(1, std::memory_order_relaxed); + gState.disagreementStreak++; + if (gState.disagreementStreak < DISAGREEMENT_STREAK_LIMIT) + { + return; + } + for (unsigned int candidate : gState.rolledBackCandidates) + { + clearFailed(candidate); + } + logLine("%u consecutive disagreements - walker suspect, %u marks rolled back, disconnecting", + gState.disagreementStreak, (unsigned int)gState.rolledBackCandidates.size()); + gState.rolledBackCandidates.clear(); + gState.disagreementStreak = 0; + gState.suspectWalkerPid.store(gState.walkerPid.load(std::memory_order_acquire), + std::memory_order_release); + dropLink("walker disagrees on every job"); +} + +inline void noteSuccess() +{ + gState.disagreementStreak = 0; + gState.deadlineStreak = 0; + gState.rolledBackCandidates.clear(); +} + +inline void applyResult(const AntWalkProto::ResultPayload& result) +{ + InFlight job; + if (!takeInFlight(result.jobId, job)) + { + return; + } + + const long long walkMs = nowMs() - job.sentAtMs; + const unsigned long long previousEma = gState.walkMsEma.load(std::memory_order_acquire); + gState.walkMsEma.store(previousEma ? (previousEma * 3 + (unsigned long long)walkMs) / 4 + : (unsigned long long)walkMs, std::memory_order_release); + gState.lastResultAtMs.store((unsigned long long)nowMs(), std::memory_order_release); + + if (result.epochId != gState.seedGeneration.load(std::memory_order_acquire) + || result.status == AntWalkProto::ResultStaleEpoch) + { + gAntColony.releaseAnnClaim(job.recordIndex); + gState.staleDropped.fetch_add(1, std::memory_order_relaxed); + logLine("job %llu dropped, epoch %u is not %u", (unsigned long long)result.jobId, + result.epochId, gState.seedGeneration.load(std::memory_order_acquire)); + return; + } + + const AntSolutionRecord* record = gAntColony.recordAt(job.recordIndex); + if (record == nullptr) + { + gAntColony.releaseAnnClaim(job.recordIndex); + return; + } + + if (result.status != AntWalkProto::ResultOk || result.score != record->score) + { + gAntColony.releaseAnnClaim(job.recordIndex); + logLine("record %u walked %u != accepted %u, marked failed", job.recordIndex, + result.score, record->score); + noteDisagreement(job.recordIndex); + return; + } + + AntColonyBpp9000T::Ann childAnn; + copyMem(&childAnn, result.childAnn, sizeof(childAnn)); + unsigned int annHash; + KangarooTwelve(&childAnn, sizeof(childAnn), &annHash, sizeof(annHash)); + + gAntColony.publishAnn(job.recordIndex, childAnn, annHash); + // The same cache every scoring path consults, so a later strict replay of this solution is a + // lookup rather than another walk. + const AntColonyBpp9000T::ReplayKey replayKey = + makeAntReplayKey(record->pubkey, record->nonce, record->parentRef, job.anchorDigest); + gAntColony.putReplayScore(replayKey, result.score, childAnn); + gState.materialised.fetch_add(1, std::memory_order_relaxed); + noteSuccess(); + if (gState.debug) + { + logLine("job %llu record %u score %u in %lld ms", (unsigned long long)result.jobId, + job.recordIndex, result.score, walkMs); + } +} + +inline void checkDeadlines() +{ + const unsigned long long ema = gState.walkMsEma.load(std::memory_order_acquire); + const long long deadlineMs = (long long)(ema * 3) > MIN_JOB_DEADLINE_MS + ? (long long)(ema * 3) : MIN_JOB_DEADLINE_MS; + const long long now = nowMs(); + + for (size_t i = 0; i < gState.inFlight.size();) + { + if (now - gState.inFlight[i].sentAtMs < deadlineMs) + { + i++; + continue; + } + const unsigned int index = gState.inFlight[i].recordIndex; + const unsigned long long jobId = gState.inFlight[i].jobId; + gState.inFlight.erase(gState.inFlight.begin() + (long)i); + gAntColony.releaseAnnClaim(index); + gState.deadlineExpiries.fetch_add(1, std::memory_order_relaxed); + // A missing result says nothing about the record, so the bitmap is left alone. + logLine("job %llu record %u no result in %lld ms, claim released, not marked", + jobId, index, deadlineMs); + gState.deadlineStreak++; + } + gState.inFlightCount.store((unsigned int)gState.inFlight.size(), std::memory_order_release); + + if (gState.deadlineStreak >= DEADLINE_STREAK_LIMIT) + { + gState.deadlineStreak = 0; + dropLink("walker alive but not answering"); + } +} + +// ── dispatcher ────────────────────────────────────────────────────────────────────────────────── + +// The colony is about to be reseeded, so every index in flight is about to mean something else. +// Claims are dropped and the scan state cleared before the reset is allowed to proceed. +inline void serveQuiesce() +{ + for (const InFlight& job : gState.inFlight) + { + gAntColony.releaseAnnClaim(job.recordIndex); + } + gState.inFlight.clear(); + gState.inFlightCount.store(0, std::memory_order_release); + gState.cursor = 0; + gState.disagreementStreak = 0; + gState.deadlineStreak = 0; + gState.rolledBackCandidates.clear(); + gState.failedBits.assign((size_t)ANT_MAX_NODES_PER_EPOCH / 8, 0); + gState.failedCount.store(0, std::memory_order_release); + gState.quiesceAcknowledged.store(true, std::memory_order_release); + + while (gState.quiesceRequested.load(std::memory_order_acquire) + && !gState.stopping.load(std::memory_order_acquire)) + { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + const int fd = gState.fd.load(std::memory_order_acquire); + if (fd >= 0 && !sendHello(fd)) + { + dropLink("hello write failed after epoch reset"); + } +} + +inline bool readOneFrame(int fd, unsigned char* payload) +{ + AntWalkProto::FrameHeader header; + if (!readFully(fd, &header, sizeof(header))) + { + dropLink("connection closed"); + return false; + } + if (header.magic != AntWalkProto::MAGIC || header.payloadSize > AntWalkProto::MAX_PAYLOAD_BYTES) + { + dropLink("bad frame"); + return false; + } + if (header.payloadSize && !readFully(fd, payload, header.payloadSize)) + { + dropLink("short frame"); + return false; + } + + if (header.type == AntWalkProto::MsgReady) + { + if (header.payloadSize != sizeof(AntWalkProto::ReadyPayload)) + { + dropLink("bad ready frame"); + return false; + } + AntWalkProto::ReadyPayload ready; + memcpy(&ready, payload, sizeof(ready)); + if ((int)ready.walkerPid == gState.suspectWalkerPid.load(std::memory_order_acquire)) + { + logLine("walker pid %u already disagreed on every job, not using it", ready.walkerPid); + dropLink("suspect walker"); + return false; + } + if (ready.status != AntWalkProto::ReadyOk) + { + logLine("handshake refused by the walker (status %u) - not retrying until it changes", + ready.status); + dropLink("handshake refused"); + return false; + } + // A ready frame also answers the hello resent when the mining seed rotates, which is a + // re-seed of a link that never dropped rather than a new connection. + const bool wasReady = + (LinkState)gState.link.load(std::memory_order_acquire) == LinkState::Ready; + gState.threadCount = ready.threadCount; + gState.walkerPid.store((int)ready.walkerPid, std::memory_order_release); + gState.link.store((int)LinkState::Ready, std::memory_order_release); + if (wasReady) + { + logLine("sidecar re-seeded for epoch %u", ready.epochId); + } + else + { + gState.reconnects.fetch_add(1, std::memory_order_relaxed); + logLine("sidecar connected, pid %u, %u threads, epoch %u", ready.walkerPid, + ready.threadCount, ready.epochId); + } + } + else if (header.type == AntWalkProto::MsgResult) + { + if (header.payloadSize != sizeof(AntWalkProto::ResultPayload)) + { + dropLink("bad result frame"); + return false; + } + AntWalkProto::ResultPayload result; + memcpy(&result, payload, sizeof(result)); + applyResult(result); + } + return true; +} + +inline void heartbeat() +{ + logLine("backlog %llu, done %llu, failed %llu, inflight %u/%u, walk avg %llu ms, link %s", + (unsigned long long)gState.backlog.load(std::memory_order_acquire), + (unsigned long long)gState.materialised.load(std::memory_order_acquire), + (unsigned long long)gState.failedCount.load(std::memory_order_acquire), + gState.inFlightCount.load(std::memory_order_acquire), gState.threadCount, + (unsigned long long)gState.walkMsEma.load(std::memory_order_acquire), + linkName((LinkState)gState.link.load(std::memory_order_acquire))); +} + +inline void dispatcherLoop() +{ + long long backoffMs = BACKOFF_MIN_MS; + long long reconnectAtMs = 0; + long long nextPingAtMs = nowMs() + PING_INTERVAL_MS; + long long nextHeartbeatAtMs = nowMs() + HEARTBEAT_INTERVAL_MS; + std::vector payload(AntWalkProto::MAX_PAYLOAD_BYTES); + + while (!gState.stopping.load(std::memory_order_acquire)) + { + if (nowMs() >= nextHeartbeatAtMs) + { + nextHeartbeatAtMs = nowMs() + HEARTBEAT_INTERVAL_MS; + refreshBacklog(); + heartbeat(); + } + + if (gState.quiesceRequested.load(std::memory_order_acquire)) + { + serveQuiesce(); + continue; + } + + // Before the first mining seed exists there is nothing to derive a pool from, and a walker + // seeded with zeros would disagree on every record. + if (score == nullptr || isZero(score->currentRandomSeed)) + { + std::this_thread::sleep_for(std::chrono::milliseconds(POLL_SLICE_MS)); + continue; + } + + const LinkState link = (LinkState)gState.link.load(std::memory_order_acquire); + if (link != LinkState::Ready && link != LinkState::Handshaking) + { + const long long now = nowMs(); + if (now < reconnectAtMs) + { + std::this_thread::sleep_for(std::chrono::milliseconds(POLL_SLICE_MS)); + continue; + } + if (!tryConnect()) + { + backoffMs = (backoffMs * 2 < BACKOFF_MAX_MS) ? backoffMs * 2 : BACKOFF_MAX_MS; + reconnectAtMs = nowMs() + backoffMs; + continue; + } + backoffMs = BACKOFF_MIN_MS; + continue; + } + + if (link == LinkState::Handshaking + && nowMs() - gState.handshakeStartedAtMs > HANDSHAKE_DEADLINE_MS) + { + dropLink("no ready frame within the handshake deadline"); + continue; + } + + const int fd = gState.fd.load(std::memory_order_acquire); + if (fd < 0) + { + continue; + } + + struct pollfd entry; + entry.fd = fd; + entry.events = POLLIN; + entry.revents = 0; + const int ready = poll(&entry, 1, POLL_SLICE_MS); + if (ready > 0 && !readOneFrame(fd, payload.data())) + { + continue; + } + if (ready < 0 && errno != EINTR) + { + dropLink("poll failed"); + continue; + } + + checkDeadlines(); + + // The queue is kept exactly as deep as the walker has threads: any more only lets a wedged + // walker hold claims the on-demand path may need. + if ((LinkState)gState.link.load(std::memory_order_acquire) == LinkState::Ready) + { + while (gState.inFlight.size() < gState.threadCount && dispatchOne()) + { + } + } + + const unsigned int generation = gState.seedGeneration.load(std::memory_order_acquire); + if (generation != gState.helloGeneration + && (LinkState)gState.link.load(std::memory_order_acquire) == LinkState::Ready) + { + const int helloFd = gState.fd.load(std::memory_order_acquire); + if (helloFd < 0 || !sendHello(helloFd)) + { + dropLink("hello write failed on seed change"); + continue; + } + } + + const long long now = nowMs(); + if (now >= nextPingAtMs) + { + nextPingAtMs = now + PING_INTERVAL_MS; + const int liveFd = gState.fd.load(std::memory_order_acquire); + if (liveFd >= 0 && !sendFrame(liveFd, AntWalkProto::MsgPing, nullptr, 0)) + { + dropLink("ping write failed"); + } + } + } + + dropLink("shutting down"); +} + +// ── node-facing api ───────────────────────────────────────────────────────────────────────────── + +inline void configure(const std::string& socketPath, unsigned int threadCount, bool debug) +{ + gState.socketPath = socketPath; + gState.threadCount = threadCount; + gState.debug = debug; +} + +inline void start() +{ + if (!isEnabled() || gState.dispatcher != nullptr) + { + return; + } + gState.stopping.store(false, std::memory_order_release); + gState.link.store((int)LinkState::Disconnected, std::memory_order_release); + gState.dispatcher = new std::thread(dispatcherLoop); +} + +inline void stop() +{ + if (gState.dispatcher == nullptr) + { + return; + } + gState.stopping.store(true, std::memory_order_release); + gState.dispatcher->join(); + delete gState.dispatcher; + gState.dispatcher = nullptr; +} + +// The seed the walker derives its pool from changed, so any result still in flight was computed +// against the old one and must not be applied. +inline void onEpochBegin() +{ + gState.seedGeneration.fetch_add(1, std::memory_order_acq_rel); +} + +inline void quiesceBegin() +{ + if (gState.dispatcher == nullptr) + { + return; + } + gState.quiesceAcknowledged.store(false, std::memory_order_release); + gState.quiesceRequested.store(true, std::memory_order_release); + + const long long startedAtMs = nowMs(); + while (!gState.quiesceAcknowledged.load(std::memory_order_acquire)) + { + if (nowMs() - startedAtMs > 5'000) + { + logLine("quiesce not acknowledged in 5000 ms, continuing"); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + logLine("quiesce for epoch reset, waited %lld ms", nowMs() - startedAtMs); +} + +inline void quiesceEnd() +{ + gState.quiesceRequested.store(false, std::memory_order_release); +} + +// Runs in the fork child, where the dispatcher thread does not exist but its descriptor was +// inherited: two readers on one socket would interleave results. +inline void closeInheritedSocket() +{ + const int fd = gState.fd.exchange(-1); + if (fd >= 0) + { + close(fd); + } + gState.link.store((int)LinkState::Disconnected, std::memory_order_release); +} + +// The promoted child owns the node now; its claims were swept by the promote path, so only the +// client's own view has to be rebuilt before a fresh dispatcher starts. +inline void restartAfterPromote() +{ + closeInheritedSocket(); + // The parent's dispatcher thread did not come through fork(); only its handle did, so the handle + // is abandoned rather than joined or destroyed. + gState.dispatcher = nullptr; + gState.inFlight.clear(); + gState.inFlightCount.store(0, std::memory_order_release); + gState.quiesceRequested.store(false, std::memory_order_release); + gState.quiesceAcknowledged.store(true, std::memory_order_release); + gState.cursor = 0; + gState.disagreementStreak = 0; + gState.deadlineStreak = 0; + gState.rolledBackCandidates.clear(); + start(); + if (isEnabled()) + { + logLine("dispatcher restarted after promote"); + } +} + +inline std::string statsJson() +{ + char buffer[768]; + snprintf(buffer, sizeof(buffer), + "{\"enabled\":%s,\"state\":\"%s\",\"socket\":\"%s\",\"threads\":%u," + "\"inflight\":%u,\"backlog\":%llu,\"materialised\":%llu,\"failed\":%llu," + "\"jobsSent\":%llu,\"disagreements\":%llu,\"deadlineExpiries\":%llu," + "\"staleDropped\":%llu,\"reconnects\":%llu,\"walkAvgMs\":%llu,\"epochId\":%u}", + isEnabled() ? "true" : "false", + linkName((LinkState)gState.link.load(std::memory_order_acquire)), + gState.socketPath.c_str(), gState.threadCount, + gState.inFlightCount.load(std::memory_order_acquire), + (unsigned long long)gState.backlog.load(std::memory_order_acquire), + (unsigned long long)gState.materialised.load(std::memory_order_acquire), + (unsigned long long)gState.failedCount.load(std::memory_order_acquire), + (unsigned long long)gState.jobsSent.load(std::memory_order_acquire), + (unsigned long long)gState.disagreements.load(std::memory_order_acquire), + (unsigned long long)gState.deadlineExpiries.load(std::memory_order_acquire), + (unsigned long long)gState.staleDropped.load(std::memory_order_acquire), + (unsigned long long)gState.reconnects.load(std::memory_order_acquire), + (unsigned long long)gState.walkMsEma.load(std::memory_order_acquire), + gState.seedGeneration.load(std::memory_order_acquire)); + return std::string(buffer); +} +} + +#else + +#include + +namespace AntWalker +{ +inline void configure(const std::string&, unsigned int, bool) {} +inline void start() {} +inline void stop() {} +inline void onEpochBegin() {} +inline void quiesceBegin() {} +inline void quiesceEnd() {} +inline void closeInheritedSocket() {} +inline void restartAfterPromote() {} +inline std::string statsJson() { return std::string("{\"enabled\":false}"); } +} + +#endif // ANT_WALKER && !_WIN32 diff --git a/src/extensions/ant_walker_proto.h b/src/extensions/ant_walker_proto.h new file mode 100644 index 00000000..6924f4a0 --- /dev/null +++ b/src/extensions/ant_walker_proto.h @@ -0,0 +1,134 @@ +#pragma once + +// Wire format shared by the node's walker client and the qubic-ant-walker sidecar. Fixed-size +// payloads only, so a frame is one read of a known length. +// +// The sidecar is a pure function of the job payload: it holds no node state and the node re-verifies +// every score against the record before publishing, so a wrong or stale sidecar can only waste its +// own CPU, never corrupt consensus. + +#include "score.h" +#include "public_settings.h" + +namespace AntWalkProto +{ +static constexpr unsigned int MAGIC = 0x57544E41u; // "ANTW" +static constexpr unsigned int VERSION = 1; + +// Bumped past the 512 MB pool build, so a walker that predates a wire change is refused at HELLO. +static constexpr unsigned int ANN_BYTES = (unsigned int)sizeof(score_engine::ScoreBpp9000T::ANN); + +// Both binaries compile the same scorer headers, so the only way they can score a nonce differently +// is a build that disagrees on the parameters feeding it. Comparing this at the handshake catches +// that before a job runs, rather than as a walker whose every result the node rejects. +constexpr unsigned int mixConfig(unsigned int accumulated, unsigned long long value) +{ + for (unsigned int byteIndex = 0; byteIndex < 8; byteIndex++) + { + accumulated = (accumulated ^ (unsigned int)((value >> (byteIndex * 8)) & 0xFF)) * 16777619u; + } + return accumulated; +} + +constexpr unsigned int CONFIG_HASH = + mixConfig(mixConfig(mixConfig(mixConfig(mixConfig(mixConfig(mixConfig(mixConfig(mixConfig(mixConfig( + 2166136261u, + BPP9000_NUMBER_OF_INPUT_NEURONS), + BPP9000_NUMBER_OF_OUTPUT_NEURONS), + BPP9000_SEQUENCE_LENGTH), + BPP9000_WINDOW_WIDTH), + BPP9000_MAX_NUMBER_OF_TICKS), + BPP9000_NUMBER_OF_NEIGHBORS), + BPP9000_POPULATION_THRESHOLD), + BPP9000_NUMBER_OF_MUTATIONS), + BPP9000_SOLUTION_THRESHOLD_DEFAULT), + (unsigned long long)ANN_BYTES); + +enum MessageType : unsigned int +{ + MsgHello = 1, // node -> walker + MsgReady = 2, // walker -> node + MsgJob = 3, // node -> walker + MsgResult = 4, // walker -> node + MsgPing = 5, // node -> walker + MsgPong = 6, // walker -> node +}; + +enum ResultStatus : unsigned int +{ + ResultOk = 0, + ResultUnscorable = 1, // the engine returned INVALID_SCORE_VALUE, childAnn is not written + ResultStaleEpoch = 2, // the job named an epoch the walker is no longer seeded for +}; + +enum ReadyStatus : unsigned int +{ + ReadyOk = 0, + ReadyVersionMismatch = 1, + ReadyTaskMismatch = 2, + ReadySeedFailed = 3, + ReadyConfigMismatch = 4, +}; + +struct FrameHeader +{ + unsigned int magic; + unsigned int type; + unsigned int payloadSize; + unsigned int reserved; +}; + +struct HelloPayload +{ + unsigned int version; + unsigned int epochId; + unsigned int annBytes; + unsigned int configHash; + unsigned char miningSeed[32]; + // The pinned task identity both binaries were compiled against; a mismatch means the two would + // score the same nonce differently, which is worth refusing before a single job runs. + unsigned char topologyHash[32]; + unsigned char dataHash[32]; +}; + +struct ReadyPayload +{ + unsigned int version; + unsigned int status; + unsigned int threadCount; + unsigned int annBytes; + unsigned int epochId; + unsigned int walkerPid; // lets the node refuse a walker it has already found to be wrong +}; + +struct JobPayload +{ + unsigned long long jobId; + unsigned int epochId; + unsigned int isRoot; // parentAnn is unused when set; the walker derives the miner's root + unsigned char pubkey[32]; + unsigned char nonce[32]; + unsigned char anchorDigest[32]; + unsigned char parentAnn[ANN_BYTES]; +}; + +struct ResultPayload +{ + unsigned long long jobId; + unsigned int epochId; + unsigned int status; + unsigned int score; + unsigned int reserved; + unsigned char childAnn[ANN_BYTES]; +}; + +static_assert(sizeof(FrameHeader) == 16, "FrameHeader must stay 16 bytes on the wire"); +static_assert(sizeof(HelloPayload) == 112, "HelloPayload layout changed - bump VERSION"); +static_assert(sizeof(ReadyPayload) == 24, "ReadyPayload layout changed - bump VERSION"); +static_assert(sizeof(JobPayload) == 16 + 96 + ANN_BYTES, "JobPayload layout changed - bump VERSION"); +static_assert(sizeof(ResultPayload) == 24 + ANN_BYTES, "ResultPayload layout changed - bump VERSION"); + +// The largest frame either side ever reads, so both can size one static buffer. +static constexpr unsigned int MAX_PAYLOAD_BYTES = + (sizeof(JobPayload) > sizeof(ResultPayload)) ? (unsigned int)sizeof(JobPayload) : (unsigned int)sizeof(ResultPayload); +} diff --git a/src/extensions/ant_walker_worker.h b/src/extensions/ant_walker_worker.h new file mode 100644 index 00000000..d449850c --- /dev/null +++ b/src/extensions/ant_walker_worker.h @@ -0,0 +1,603 @@ +#pragma once + +// The ant score walk, run in a separate process re-exec'd from this same binary (the shim passes +// --ant-walk-worker). A walk holds a score-engine lock for tens of seconds while a checkpoint window +// is only ~21 s, so on a node thread the fork census would skip nearly every fork and force those +// ticks strict. Here the node only sends a job and applies a verified result. +// +// Stateless: the pool is re-derived from the seed the node sends and the task is the blob compiled +// into this binary. Nothing about a node's colony lives here, and the node re-verifies every score +// against the record before publishing it. + +#if defined(ANT_WALKER) && !defined(_WIN32) + +#include "ant_walker_proto.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#if defined(__linux__) +#include +#endif + +namespace AntWalkerWorker +{ +using Ann = score_engine::ScoreBpp9000T::ANN; + +constexpr int NO_TRAFFIC_TIMEOUT_MS = 60'000; +constexpr int POLL_SLICE_MS = 1'000; + +struct Options +{ + std::string socketPath; + unsigned int threadCount = 4; +}; + +// ── socket helpers ────────────────────────────────────────────────────────────────────────────── + +bool readFully(int fd, void* buffer, size_t size) +{ + unsigned char* out = (unsigned char*)buffer; + size_t done = 0; + while (done < size) + { + const ssize_t got = read(fd, out + done, size - done); + if (got > 0) + { + done += (size_t)got; + continue; + } + if (got < 0 && errno == EINTR) + { + continue; + } + return false; + } + return true; +} + +bool writeFully(int fd, const void* buffer, size_t size) +{ + const unsigned char* in = (const unsigned char*)buffer; + size_t done = 0; + while (done < size) + { + const ssize_t put = write(fd, in + done, size - done); + if (put > 0) + { + done += (size_t)put; + continue; + } + if (put < 0 && errno == EINTR) + { + continue; + } + return false; + } + return true; +} + +// Waits for readable with a bounded total timeout, so a peer that vanished without an EOF cannot +// leave this process parked forever on a connection that will never speak again. +bool waitReadable(int fd, int timeoutMs, const std::atomic& stop) +{ + int waitedMs = 0; + while (waitedMs < timeoutMs) + { + if (stop.load(std::memory_order_acquire)) + { + return false; + } + struct pollfd entry; + entry.fd = fd; + entry.events = POLLIN; + entry.revents = 0; + const int slice = (timeoutMs - waitedMs < POLL_SLICE_MS) ? (timeoutMs - waitedMs) : POLL_SLICE_MS; + const int ready = poll(&entry, 1, slice); + if (ready > 0) + { + return true; + } + if (ready < 0 && errno != EINTR) + { + return false; + } + waitedMs += slice; + } + return false; +} + +// ── engine ────────────────────────────────────────────────────────────────────────────────────── + +unsigned char* gPool = nullptr; +unsigned char gPoolSeed[32] = {}; +bool gPoolSeeded = false; + +bool ensurePool(const unsigned char* miningSeed) +{ + if (gPool == nullptr) + { + gPool = (unsigned char*)malloc((size_t)score_engine::POOL_VEC_PADDING_SIZE); + if (gPool == nullptr) + { + return false; + } + } + if (gPoolSeeded && memcmp(gPoolSeed, miningSeed, 32) == 0) + { + return true; + } + unsigned char state[score_engine::STATE_SIZE]; + score_engine::generateRandom2Pool(miningSeed, state, gPool); + memcpy(gPoolSeed, miningSeed, 32); + gPoolSeeded = true; + return true; +} + +bool loadEmbeddedTask(score_engine::ScoreBpp9000T& engine) +{ + const unsigned int inputTrits = (unsigned int)BPP9000_NUMBER_OF_INPUT_NEURONS; + const unsigned int outputTrits = (unsigned int)BPP9000_NUMBER_OF_OUTPUT_NEURONS; + const unsigned int population = (unsigned int)BPP9000_POPULATION_THRESHOLD; + const unsigned int neighbors = (unsigned int)BPP9000_NUMBER_OF_NEIGHBORS; + + const unsigned long long topologyBytes = + score_task_file::topologyBytes(inputTrits, outputTrits, population, neighbors); + const unsigned char* topologyBlock = BPP9000_TASK_BYTES + sizeof(score_task_file::TaskFileHeader); + const unsigned char* dataBlock = topologyBlock + topologyBytes; + + engine.initMemory(); + return engine.loadTaskFromMemory(topologyBlock, dataBlock); +} + +// ── worker pool ───────────────────────────────────────────────────────────────────────────────── + +struct WorkerPool +{ + std::mutex queueMutex; // SMARTMUTEX-EXEMPT: separate process, holds no node state and never forks + std::condition_variable queueSignal; + std::deque queue; + + std::mutex writeMutex; // SMARTMUTEX-EXEMPT: separate process, holds no node state and never forks + std::atomic stopping{ false }; + std::atomic connected{ false }; + std::atomic epochId{ 0 }; + std::atomic connectionFd{ -1 }; + std::atomic busy{ 0 }; + + std::vector threads; +}; + +WorkerPool gPoolOfWorkers; + +void runWorker(unsigned int workerIndex) +{ + score_engine::ScoreBpp9000T* engine = (score_engine::ScoreBpp9000T*)aligned_alloc(64, + (sizeof(score_engine::ScoreBpp9000T) + 63) / 64 * 64); + if (engine == nullptr || !loadEmbeddedTask(*engine)) + { + fprintf(stderr, "[ant-walker] worker %u could not load the embedded task\n", workerIndex); + fflush(stderr); + return; + } + Ann rootAnn; + Ann childAnn; + + for (;;) + { + AntWalkProto::JobPayload job; + { + std::unique_lock lock(gPoolOfWorkers.queueMutex); + gPoolOfWorkers.queueSignal.wait(lock, [] + { + return gPoolOfWorkers.stopping.load(std::memory_order_acquire) + || !gPoolOfWorkers.queue.empty(); + }); + if (gPoolOfWorkers.stopping.load(std::memory_order_acquire) && gPoolOfWorkers.queue.empty()) + { + break; + } + job = gPoolOfWorkers.queue.front(); + gPoolOfWorkers.queue.pop_front(); + gPoolOfWorkers.busy.fetch_add(1, std::memory_order_acq_rel); + } + + struct BusyGuard + { + ~BusyGuard() { gPoolOfWorkers.busy.fetch_sub(1, std::memory_order_acq_rel); } + } busyGuard; + + AntWalkProto::ResultPayload result; + memset(&result, 0, sizeof(result)); + result.jobId = job.jobId; + result.epochId = job.epochId; + + if (job.epochId != gPoolOfWorkers.epochId.load(std::memory_order_acquire)) + { + result.status = AntWalkProto::ResultStaleEpoch; + } + else + { + const Ann* parentAnn; + if (job.isRoot) + { + engine->deriveRootANN(job.pubkey, gPool, rootAnn); + parentAnn = &rootAnn; + } + else + { + parentAnn = (const Ann*)job.parentAnn; + } + + const unsigned int score = engine->computeScoreFromParent( + *parentAnn, job.pubkey, job.nonce, job.anchorDigest, gPool); + if (score == score_engine::INVALID_SCORE_VALUE) + { + result.status = AntWalkProto::ResultUnscorable; + } + else + { + engine->getBestANN(childAnn); + result.status = AntWalkProto::ResultOk; + result.score = score; + memcpy(result.childAnn, &childAnn, sizeof(childAnn)); + } + } + + // A walk cannot be aborted mid-flight, so one that outlived its connection is finished and + // then dropped rather than written to whatever now owns that descriptor. + if (!gPoolOfWorkers.connected.load(std::memory_order_acquire)) + { + continue; + } + std::lock_guard writeGuard(gPoolOfWorkers.writeMutex); + const int fd = gPoolOfWorkers.connectionFd.load(std::memory_order_acquire); + if (fd < 0) + { + continue; + } + AntWalkProto::FrameHeader header; + header.magic = AntWalkProto::MAGIC; + header.type = AntWalkProto::MsgResult; + header.payloadSize = (unsigned int)sizeof(result); + header.reserved = 0; + if (!writeFully(fd, &header, sizeof(header)) || !writeFully(fd, &result, sizeof(result))) + { + gPoolOfWorkers.connected.store(false, std::memory_order_release); + } + } + + free(engine); +} + +// ── connection ────────────────────────────────────────────────────────────────────────────────── + +bool sendReady(int fd, unsigned int status, unsigned int threadCount, unsigned int epochId) +{ + AntWalkProto::ReadyPayload ready; + memset(&ready, 0, sizeof(ready)); + ready.version = AntWalkProto::VERSION; + ready.status = status; + ready.threadCount = threadCount; + ready.annBytes = AntWalkProto::ANN_BYTES; + ready.epochId = epochId; + ready.walkerPid = (unsigned int)getpid(); + + AntWalkProto::FrameHeader header; + header.magic = AntWalkProto::MAGIC; + header.type = AntWalkProto::MsgReady; + header.payloadSize = (unsigned int)sizeof(ready); + header.reserved = 0; + + std::lock_guard writeGuard(gPoolOfWorkers.writeMutex); + return writeFully(fd, &header, sizeof(header)) && writeFully(fd, &ready, sizeof(ready)); +} + +unsigned int validateHello(const AntWalkProto::HelloPayload& hello) +{ + if (hello.version != AntWalkProto::VERSION || hello.annBytes != AntWalkProto::ANN_BYTES) + { + return AntWalkProto::ReadyVersionMismatch; + } + if (memcmp(hello.topologyHash, BPP9000_TOPOLOGY_HASH, 32) != 0 + || memcmp(hello.dataHash, BPP9000_DATA_HASH, 32) != 0) + { + return AntWalkProto::ReadyTaskMismatch; + } + if (hello.configHash != AntWalkProto::CONFIG_HASH) + { + fprintf(stderr, "[ant-walker] scorer config %08x does not match the node's %08x\n", + AntWalkProto::CONFIG_HASH, hello.configHash); + fflush(stderr); + return AntWalkProto::ReadyConfigMismatch; + } + return AntWalkProto::ReadyOk; +} + +bool hasWorkOutstanding() +{ + if (gPoolOfWorkers.busy.load(std::memory_order_acquire) > 0) + { + return true; + } + std::lock_guard queueGuard(gPoolOfWorkers.queueMutex); + return !gPoolOfWorkers.queue.empty(); +} + +void serveConnection(int fd, unsigned int threadCount) +{ + gPoolOfWorkers.connectionFd.store(fd, std::memory_order_release); + gPoolOfWorkers.connected.store(true, std::memory_order_release); + + unsigned char payload[AntWalkProto::MAX_PAYLOAD_BYTES]; + bool handshaken = false; + + while (gPoolOfWorkers.connected.load(std::memory_order_acquire)) + { + if (!waitReadable(fd, NO_TRAFFIC_TIMEOUT_MS, gPoolOfWorkers.stopping)) + { + // A single walk runs far longer than this timeout, so silence only means a dead peer + // when there is nothing outstanding to answer with. + if (hasWorkOutstanding()) + { + continue; + } + fprintf(stderr, "[ant-walker] no traffic for %d ms and nothing in flight, dropping the connection\n", + NO_TRAFFIC_TIMEOUT_MS); + fflush(stderr); + break; + } + + AntWalkProto::FrameHeader header; + if (!readFully(fd, &header, sizeof(header))) + { + break; + } + if (header.magic != AntWalkProto::MAGIC || header.payloadSize > sizeof(payload)) + { + fprintf(stderr, "[ant-walker] bad frame (magic %08x size %u), dropping the connection\n", + header.magic, header.payloadSize); + fflush(stderr); + break; + } + if (header.payloadSize && !readFully(fd, payload, header.payloadSize)) + { + break; + } + + if (header.type == AntWalkProto::MsgHello) + { + if (header.payloadSize != sizeof(AntWalkProto::HelloPayload)) + { + break; + } + AntWalkProto::HelloPayload hello; + memcpy(&hello, payload, sizeof(hello)); + + unsigned int status = validateHello(hello); + if (status == AntWalkProto::ReadyOk && !ensurePool(hello.miningSeed)) + { + status = AntWalkProto::ReadySeedFailed; + } + if (status == AntWalkProto::ReadyOk) + { + gPoolOfWorkers.epochId.store(hello.epochId, std::memory_order_release); + handshaken = true; + } + if (!sendReady(fd, status, threadCount, hello.epochId) || status != AntWalkProto::ReadyOk) + { + fprintf(stderr, "[ant-walker] handshake refused (status %u)\n", status); + fflush(stderr); + break; + } + fprintf(stderr, "[ant-walker] ready, %u threads, epoch %u\n", threadCount, hello.epochId); + fflush(stderr); + } + else if (header.type == AntWalkProto::MsgJob) + { + if (!handshaken || header.payloadSize != sizeof(AntWalkProto::JobPayload)) + { + break; + } + std::lock_guard queueGuard(gPoolOfWorkers.queueMutex); + gPoolOfWorkers.queue.emplace_back(); + memcpy(&gPoolOfWorkers.queue.back(), payload, sizeof(AntWalkProto::JobPayload)); + gPoolOfWorkers.queueSignal.notify_one(); + } + else if (header.type == AntWalkProto::MsgPing) + { + AntWalkProto::FrameHeader pong; + pong.magic = AntWalkProto::MAGIC; + pong.type = AntWalkProto::MsgPong; + pong.payloadSize = 0; + pong.reserved = 0; + std::lock_guard writeGuard(gPoolOfWorkers.writeMutex); + if (!writeFully(fd, &pong, sizeof(pong))) + { + break; + } + } + } + + gPoolOfWorkers.connected.store(false, std::memory_order_release); + { + // Queued jobs belong to a connection that is gone; only the walks already running are paid for. + std::lock_guard queueGuard(gPoolOfWorkers.queueMutex); + gPoolOfWorkers.queue.clear(); + } + { + // Held until every worker that might still write is out of writeFully, so the descriptor is + // never closed under one of them. + std::lock_guard writeGuard(gPoolOfWorkers.writeMutex); + gPoolOfWorkers.connectionFd.store(-1, std::memory_order_release); + } + close(fd); +} + +// ── listener ──────────────────────────────────────────────────────────────────────────────────── + +// A live server on this path means this process would silently compete with it for the node's jobs, +// which is how an orphaned sidecar ends up answering for a running one. Only a socket nothing answers +// on is stale enough to replace. +bool socketPathIsServed(const char* path) +{ + const int probeFd = socket(AF_UNIX, SOCK_STREAM, 0); + if (probeFd < 0) + { + return false; + } + struct sockaddr_un address; + memset(&address, 0, sizeof(address)); + address.sun_family = AF_UNIX; + snprintf(address.sun_path, sizeof(address.sun_path), "%s", path); + const bool served = connect(probeFd, (struct sockaddr*)&address, sizeof(address)) == 0; + close(probeFd); + return served; +} + +int openListener(const char* path) +{ + if (socketPathIsServed(path)) + { + fprintf(stderr, "[ant-walker] %s is already served, refusing to compete\n", path); + fflush(stderr); + return -1; + } + unlink(path); + + const int listenFd = socket(AF_UNIX, SOCK_STREAM, 0); + if (listenFd < 0) + { + perror("[ant-walker] socket"); + return -1; + } + struct sockaddr_un address; + memset(&address, 0, sizeof(address)); + address.sun_family = AF_UNIX; + snprintf(address.sun_path, sizeof(address.sun_path), "%s", path); + if (bind(listenFd, (struct sockaddr*)&address, sizeof(address)) != 0 || listen(listenFd, 4) != 0) + { + perror("[ant-walker] bind/listen"); + close(listenFd); + return -1; + } + return listenFd; +} + +bool parseOptions(int argc, const char* argv[], Options& options) +{ + for (int i = 1; i + 1 < argc; i++) + { + const std::string argument = argv[i]; + if (argument == "--socket") + { + options.socketPath = argv[i + 1]; + } + else if (argument == "--threads") + { + options.threadCount = (unsigned int)strtoul(argv[i + 1], nullptr, 10); + } + } + return !options.socketPath.empty() && options.threadCount > 0; +} + +// The shim re-execs this binary with the worker flag rather than shipping a second one; the node +// half of main() must not run here, so this is checked before any node setup. +inline bool requested(int argc, const char* argv[]) +{ + for (int i = 1; i < argc; i++) + { + if (std::string(argv[i]) == "--ant-walk-worker") + { + return true; + } + } + return false; +} + +inline int run(int argc, const char* argv[]) +{ + Options options; + if (!parseOptions(argc, argv, options)) + { + fprintf(stderr, "[ant-walker] --ant-walk-worker needs --socket --threads \n"); + return 2; + } + + signal(SIGPIPE, SIG_IGN); +#if defined(__linux__) + // The node is the only reason this process exists; outliving it would just squat the socket. + prctl(PR_SET_PDEATHSIG, SIGTERM); + if (getppid() == 1) + { + return 0; + } +#endif + + const int listenFd = openListener(options.socketPath.c_str()); + if (listenFd < 0) + { + return 3; + } + + for (unsigned int i = 0; i < options.threadCount; i++) + { + gPoolOfWorkers.threads.emplace_back(runWorker, i); + } + fprintf(stderr, "[ant-walker] listening on %s, %u threads, pid %d\n", + options.socketPath.c_str(), options.threadCount, (int)getpid()); + fflush(stderr); + + while (!gPoolOfWorkers.stopping.load(std::memory_order_acquire)) + { + struct pollfd entry; + entry.fd = listenFd; + entry.events = POLLIN; + entry.revents = 0; + const int ready = poll(&entry, 1, POLL_SLICE_MS); +#if defined(__linux__) + if (getppid() == 1) + { + break; + } +#endif + if (ready <= 0) + { + continue; + } + const int connectionFd = accept(listenFd, nullptr, nullptr); + if (connectionFd < 0) + { + continue; + } + serveConnection(connectionFd, options.threadCount); + } + + gPoolOfWorkers.stopping.store(true, std::memory_order_release); + gPoolOfWorkers.queueSignal.notify_all(); + for (std::thread& worker : gPoolOfWorkers.threads) + { + worker.join(); + } + close(listenFd); + unlink(options.socketPath.c_str()); + return 0; +} +} + +#else + +namespace AntWalkerWorker +{ +inline bool requested(int, const char*[]) { return false; } +inline int run(int, const char*[]) { return 0; } +} + +#endif // ANT_WALKER && !_WIN32 diff --git a/src/extensions/http/controller/rpc_stats_controller.h b/src/extensions/http/controller/rpc_stats_controller.h index 7453d545..0535f76b 100644 --- a/src/extensions/http/controller/rpc_stats_controller.h +++ b/src/extensions/http/controller/rpc_stats_controller.h @@ -140,6 +140,16 @@ RPC_ROUTE("GET", "/v1/fork-stats") return r; } +// Ant walker sidecar health: link state, backlog and the counters that tell a walker chewing through +// work from one that is up but delivering nothing. +RPC_ROUTE("GET", "/v1/ant-walker") +{ + (void)req; + RpcResp r; + r.body = AntWalker::statsJson(); + return r; +} + // The full durable record of every unforkable tick (not a recent ring) — one line per skipped fork. RPC_ROUTE("GET", "/v1/unforkable-ticks") { diff --git a/src/extensions/supervisor_shim.h b/src/extensions/supervisor_shim.h index e8015433..0ff17260 100644 --- a/src/extensions/supervisor_shim.h +++ b/src/extensions/supervisor_shim.h @@ -16,6 +16,7 @@ #include inline char gSidecarPort[16] = "41841"; // node http port -> sidecar listen + unix-socket key +inline char gAntWalkerThreads[16] = "4"; // matches the node default; 0 keeps the walker unspawned // Forward a stop signal to the children so the container/service stops promptly. static void shimForwardSignal(int sig) @@ -49,8 +50,40 @@ static pid_t shimForkSidecar() #endif } +// Re-exec self as the ant walker, a sibling of the node: the ant score walk runs there so the node +// never holds a score-engine lock across a checkpoint fork point. Spawned here rather than by the +// node so it outlives a rollback promotion. +static pid_t shimForkAntWalker() +{ + if (std::atoi(gAntWalkerThreads) <= 0) + return -1; + + const pid_t supervisorPid = getpid(); + pid_t walkerPid = fork(); + if (walkerPid != 0) + return walkerPid; + prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0); + if (getppid() != supervisorPid) + _exit(0); + + char self[512]; + ssize_t pathLength = readlink("/proc/self/exe", self, sizeof(self) - 1); + if (pathLength <= 0) + _exit(127); + self[pathLength] = 0; + + char socketPath[128]; + snprintf(socketPath, sizeof(socketPath), "/tmp/qubic-antwalk-%s.sock", gSidecarPort); + execl(self, "qubic-ant-walker", "--ant-walk-worker", "--socket", socketPath, + "--threads", gAntWalkerThreads, (char*)nullptr); + // Not fatal for the node: without a walker the backlog is simply paid on demand as before. + fprintf(stderr, "[shim] could not exec the ant walker (%s), running without it\n", strerror(errno)); + fflush(stderr); + _exit(127); +} + // True while any child other than the sidecar exists (i.e. the node lineage is still alive). -static bool shimHasNodeChild(pid_t sidecar) +static bool shimHasNodeChild(pid_t sidecar, pid_t antWalker) { char path[64]; snprintf(path, sizeof(path), "/proc/self/task/%d/children", (int)getpid()); @@ -61,7 +94,7 @@ static bool shimHasNodeChild(pid_t sidecar) bool hasNodeChild = false; while (fscanf(childrenFile, "%d", &childPid) == 1) { - if (childPid != (int)sidecar) + if (childPid != (int)sidecar && childPid != (int)antWalker) { hasNodeChild = true; break; @@ -85,12 +118,19 @@ static inline void runUnderSupervisor(int argc, const char** argv) std::strncpy(gSidecarPort, argv[i + 1], sizeof(gSidecarPort) - 1); gSidecarPort[sizeof(gSidecarPort) - 1] = 0; } + if (std::string(argv[i]) == "--ant-walker-threads" && i + 1 < argc) + { + std::strncpy(gAntWalkerThreads, argv[i + 1], sizeof(gAntWalkerThreads) - 1); + gAntWalkerThreads[sizeof(gAntWalkerThreads) - 1] = 0; + } } if (prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) != 0) // can't subreap: run node inline return; pid_t sidecar = shimForkSidecar(); + pid_t antWalker = shimForkAntWalker(); + int antWalkerRestarts = 0; pid_t node = fork(); if (node < 0) @@ -121,14 +161,31 @@ static inline void runUnderSupervisor(int argc, const char** argv) sidecar = shimForkSidecar(); // RPC must not stay down: restart it continue; } + if (antWalker > 0 && reapedPid == antWalker) + { + sleep(1); // a squatted socket would hot-loop the respawn + // A missing or unrunnable walker binary would otherwise respawn once a second forever; + // the node works without one, it just pays the walks on demand. + if (++antWalkerRestarts > 5) + { + fprintf(stderr, "[shim] ant walker died %d times, leaving it down\n", antWalkerRestarts); + fflush(stderr); + antWalker = -1; + continue; + } + antWalker = shimForkAntWalker(); + continue; + } lastStatus = status; if (WIFSIGNALED(status)) sawSignal = true; - if (!shimHasNodeChild(sidecar)) + if (!shimHasNodeChild(sidecar, antWalker)) break; // node lineage drained -> shim exits } if (sidecar > 0) kill(sidecar, SIGTERM); + if (antWalker > 0) + kill(antWalker, SIGTERM); _exit(sawSignal ? 1 : (WIFEXITED(lastStatus) ? WEXITSTATUS(lastStatus) : 1)); } diff --git a/src/platform/concurrency.h b/src/platform/concurrency.h index 41670cbe..980ece2b 100644 --- a/src/platform/concurrency.h +++ b/src/platform/concurrency.h @@ -40,6 +40,9 @@ namespace ForkCensus { std::atomic depth{ 0 }; std::atomic what{ nullptr }; + // Every LockGuard-based lock reports the same `what` (the macro expands inside LockGuard's + // constructor), so the address is the only thing that says which lock blocked a fork. + std::atomic where{ nullptr }; std::atomic live{ 0 }; // 0 = free/reusable, 1 = owned by a live thread }; inline Slot gSlots[MAX_THREADS]; @@ -99,13 +102,14 @@ namespace ForkCensus } } - inline void enter(const char* what) + inline void enter(const char* what, const volatile void* where = nullptr) { if (tlLockSlot < 0) claimSlot(); if (tlLockSlot < 0) return; // registry full (>MAX_THREADS live): best-effort, this thread uncounted gSlots[tlLockSlot].what.store(what, std::memory_order_relaxed); + gSlots[tlLockSlot].where.store(where, std::memory_order_relaxed); gSlots[tlLockSlot].depth.fetch_add(1, std::memory_order_relaxed); } inline void leave() @@ -131,6 +135,22 @@ namespace ForkCensus } return heldDepth < 0 ? 0 : heldDepth; } + inline const volatile void* offenderAddress() + { + const int selfSlot = tlLockSlot; + int slotCount = gCount.load(std::memory_order_acquire); + if (slotCount > MAX_THREADS) + slotCount = MAX_THREADS; + for (int slotIndex = 0; slotIndex < slotCount; slotIndex++) + { + if (slotIndex != selfSlot && gSlots[slotIndex].depth.load(std::memory_order_relaxed) > 0) + { + return gSlots[slotIndex].where.load(std::memory_order_relaxed); + } + } + return nullptr; + } + inline const char* offenderName() { if (gOverflow.load(std::memory_order_acquire)) @@ -157,6 +177,7 @@ namespace ForkCensus { gSlots[i].depth.store(0, std::memory_order_relaxed); gSlots[i].what.store(nullptr, std::memory_order_relaxed); + gSlots[i].where.store(nullptr, std::memory_order_relaxed); gSlots[i].live.store(0, std::memory_order_relaxed); } gCount.store(0, std::memory_order_release); @@ -165,9 +186,9 @@ namespace ForkCensus } } -inline void forkCensusEnter(const char* what) +inline void forkCensusEnter(const char* what, const volatile void* where = nullptr) { - ForkCensus::enter(what); + ForkCensus::enter(what, where); } inline void forkCensusLeave() @@ -185,6 +206,11 @@ inline const char* forkCensusOffender() return ForkCensus::offenderName(); } +inline const volatile void* forkCensusOffenderAddress() +{ + return ForkCensus::offenderAddress(); +} + inline void forkCensusResetForChildPromote() { ForkCensus::resetForChildPromote(); @@ -199,7 +225,7 @@ inline bool gForkCensus = true; do { \ while (_InterlockedCompareExchange8(&lock, 1, 0)) \ _mm_pause(); \ - forkCensusEnter(#lock " @ " __FILE__); \ + forkCensusEnter(#lock " @ " __FILE__, &lock); \ } while (0) #ifdef NDEBUG @@ -232,7 +258,7 @@ class BusyWaitingTracker while (_InterlockedCompareExchange8(&lock, 1, 0)) \ bwt.pause(); \ } \ - forkCensusEnter(#lock " @ " __FILE__); \ + forkCensusEnter(#lock " @ " __FILE__, &lock); \ } while (0) #endif @@ -240,7 +266,7 @@ class BusyWaitingTracker // Try to acquire lock and return if successful (without blocking) #define TRY_ACQUIRE(lock) \ (_InterlockedCompareExchange8(&lock, 1, 0) == 0 \ - ? (forkCensusEnter(#lock " @ " __FILE__), true) \ + ? (forkCensusEnter(#lock " @ " __FILE__, &lock), true) \ : false) // Release lock diff --git a/src/qubic.cpp b/src/qubic.cpp index 0086be9a..8ae46662 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -767,9 +767,18 @@ static bool materialiseOneAntRecord(unsigned long long processorNumber, unsigned return false; } + // Same cache every other scoring path consults, so a rebuild never re-walks a score this node + // already holds - including one restored from the on-disk replay cache. AntColonyBpp9000T::Ann& childAnn = gAntRebuildChildScratch[processorNumber]; - const unsigned int rebuiltScore = score->computeAntChildScore(processorNumber, parentAnn, - rec->pubkey, rec->nonce, anchorDigest, childAnn); + const AntColonyBpp9000T::ReplayKey replayKey = + makeAntReplayKey(rec->pubkey, rec->nonce, rec->parentRef, anchorDigest); + unsigned int rebuiltScore; + if (!gAntColony.tryGetReplayScore(replayKey, rebuiltScore, childAnn)) + { + rebuiltScore = score->computeAntChildScore(processorNumber, parentAnn, + rec->pubkey, rec->nonce, anchorDigest, childAnn); + gAntColony.putReplayScore(replayKey, rebuiltScore, childAnn); + } // This walk is the computation the record was admitted without. A disagreement means the acceptance // was wrong, so publish nothing: a network built for a score no other node holds is worse than none. @@ -860,6 +869,10 @@ static bool ensureAntRecordAnn(unsigned long long processorNumber, unsigned int return (rec != nullptr) && gAntColony.annOfNonRoot(*rec, out); } +#include "extensions/ant_colony_maintenance.h" +#include "extensions/ant_walker_worker.h" +#include "extensions/ant_walker_client.h" + // A pool miner's solution, arriving over BroadcastMessage static void queueAntSolution(unsigned long long processorNumber, const m256i& computorPublicKey, const AntSolutionBroadcastPayload& payload) @@ -969,8 +982,11 @@ static void antColonyBeginEpoch() #ifndef NDEBUG gAntDebugPrintBudget = ANT_DEBUG_PRINTS_PER_EPOCH; #endif + AntWalker::quiesceBegin(); gAntPendingSolutions.reset(); gAntColony.beginEpoch(score->currentRandomSeed, system.initialTick); + AntWalker::onEpochBegin(); + AntWalker::quiesceEnd(); gAntColony.setErrorThreshold((unsigned int)getSolutionThreshold(score_engine::AlgoType::Bpp9000)); // Every identity's root derives from this one value, so a node that seeded differently builds a @@ -9490,6 +9506,7 @@ static void deinitialize() fastTxWindow.deinit(); gAntPendingSolutions.deinit(); + AntWalker::stop(); gAntColony.deinit(); if (score) @@ -10620,6 +10637,13 @@ static void tickForkChildPromote(unsigned int strictUntilTick) const bool shadowClean = gShadow.purgeOrphans(); ts.resetSwapPinsForChildPromote(); forkCensusResetForChildPromote(); + const unsigned int releasedAntClaims = AntColonyMaintenance::releaseInheritedClaims(gAntColony); + AntWalker::restartAfterPromote(); + if (releasedAntClaims) + { + fprintf(stderr, "[FORK] CHILD: released %u inherited ant network claims\n", releasedAntClaims); + fflush(stderr); + } if (!shadowClean) { forceVerifySolutions = true; @@ -10811,8 +10835,8 @@ static void bspForkPoint() { const char* offendingLock = forkCensusOffender(); ForkStats::onForkSkipped(ForkStats::CENSUS, (unsigned)system.tick, offendingLock ? offendingLock : "?"); - fprintf(stderr, "[FORK] census: non-BSP thread holds '%s' -> skip fork, run tick %u strict\n", offendingLock ? offendingLock : "?", - (unsigned)system.tick); + fprintf(stderr, "[FORK] census: non-BSP thread holds '%s' (%p) -> skip fork, run tick %u strict\n", + offendingLock ? offendingLock : "?", (const void*)forkCensusOffenderAddress(), (unsigned)system.tick); fflush(stderr); quiescence.release(); tickFork::gChildPid = -1; @@ -10841,6 +10865,7 @@ static void bspForkPoint() { // CHILD BSP: block until parent's verdict, then become the node. quiescence.abandonInChild(); + AntWalker::closeInheritedSocket(); close(tickFork::gPipe[1]); const auto childCommand = tickForkControl::readChildCommand(tickFork::gPipe[0], (unsigned)system.tick + tickFork::gForkWindowK); if (childCommand.action == tickForkControl::ChildAction::Retire) @@ -11761,6 +11786,8 @@ void processArgs(int argc, const char* argv[]) { ("fbas-warmup", "TEST: publish this many valid ant solutions before switching to the --fbas mode", cxxopts::value()->default_value("0")) ("fbas-gap", "TEST: minimum ticks between ant publishes; a gap wider than the fork window makes each window retire", cxxopts::value()->default_value("0")) ("ant-debug", "Trace ant-colony accepts, over-accepts and network rebuilds (budgeted per epoch)", cxxopts::value()) + ("ant-walker-threads", "Ant network walks handed to the walker sidecar (0=off)", cxxopts::value()->default_value("4")) + ("ant-walker-debug", "Trace every ant walker job and result", cxxopts::value()) #if defined(__linux__) && !defined(LITE_WASM_SC) ("verify-fork-rollback", "TEST: assert fork re-run reproduces quorum digest", cxxopts::value()) ("fork-force-fork", "TEST: fork every tick (exercise MATCH path)", cxxopts::value()) @@ -11982,6 +12009,18 @@ void processArgs(int argc, const char* argv[]) { + std::to_string(ANT_DEBUG_PRINTS_PER_EPOCH) + " lines per epoch"); } + { + const unsigned int antWalkerThreads = result["ant-walker-threads"].as(); + char antWalkerSocket[128]; + snprintf(antWalkerSocket, sizeof(antWalkerSocket), "/tmp/qubic-antwalk-%d.sock", httpPort); + AntWalker::configure(antWalkerSocket, antWalkerThreads, result.count("ant-walker-debug") > 0); + if (antWalkerThreads > 0) + { + logColorToScreen("INFO", "Ant walker sidecar enabled, " + std::to_string(antWalkerThreads) + + " threads, socket " + antWalkerSocket); + } + } + if (result.count("rebuild-tx-hashmap")) { rebuildTxHashmap = true; @@ -12386,6 +12425,12 @@ int main(int argc, const char* argv[]) return tickStorageScan::scan(); } #endif + // Before any node setup: this process is the walker sidecar, not a node. It leaves via _exit so + // the node's static destructors never run against globals this process never initialised. + if (AntWalkerWorker::requested(argc, argv)) + { + _exit(AntWalkerWorker::run(argc, argv)); + } #if defined(__linux__) && !defined(NO_RPC) && !defined(LITE_WASM_SC) int rpcProxyExitCode = 0; if (runRpcProxyIfRequested(argc, argv, rpcProxyExitCode)) @@ -12402,6 +12447,7 @@ int main(int argc, const char* argv[]) #if defined(__linux__) && !defined(NO_RPC) && !defined(LITE_WASM_SC) startRpcServices(); #endif + AntWalker::start(); #if defined(LITE_WASM_SC) && !defined(NO_RPC) // Wasm testnet serves HTTP in-process; the unix-socket/sidecar stack is compiled out. QubicHttpServer::start(httpPort); diff --git a/test/ant_colony.cpp b/test/ant_colony.cpp index 6a93959e..bd2f5434 100644 --- a/test/ant_colony.cpp +++ b/test/ant_colony.cpp @@ -6,6 +6,7 @@ // The bound colony, not the bare template: these tests check bpp9000's binding as well as the rules. #include "../src/mining/ant_colony/ant_colony_bpp9000.h" +#include "../src/extensions/ant_colony_maintenance.h" #include @@ -1146,3 +1147,104 @@ TEST(TestAntColonyExport, BeginEpochClearsIt) EXPECT_EQ(header.entryCount, 0u); EXPECT_EQ(header.solutionCount, 0u); } + +// A child of an existing node committed on trust: no network, the way an AUX node stores one. +static long long commitChildWithoutAnn(AntColonyBpp9000T* colony, const m256i& owner, + const SolutionRef& parentRef, unsigned int score, unsigned int txIdx, unsigned long long nonceSeed, + unsigned int tick = 100000) +{ + const AntSolutionRecord* parentRec = nullptr; + if (colony->tryGetParent(parentRef, &parentRec) != ValidityResult::Valid) + { + return ANT_INVALID_INDEX; + } + + AntCommitInput in; + in.pubkey = owner; + in.nonce = makeKey(nonceSeed); + in.parentRef = parentRef; + in.selfRef.tick = tick; + in.selfRef.solutionIndexInTick = txIdx; + in.anchorTick = tick; + in.publishTick = tick; + + const long long landsAt = (long long)colony->solutionCount(); + if (colony->commit(in, parentRec, score, nullptr, 0, true) != ValidityResult::Valid) + { + return ANT_INVALID_INDEX; + } + return landsAt; +} + +// fork() clones one thread, so a claim held at fork time has no owner in the child and the on-demand +// waiter would spin on it forever. +TEST(TestAntColonyMaintenance, PromoteReleasesAnInheritedClaim) +{ + AntColonyBpp9000T* colony = freshColony(); + ASSERT_NE(colony, nullptr) << "colony init failed; needs ~6.2 GB"; + + const m256i me = makeKey(41); + const long long idx = commitRootChildWithoutAnn(colony, me, 3800, 0, 901); + ASSERT_NE(idx, ANT_INVALID_INDEX); + const unsigned int slot = (unsigned int)idx; + + ASSERT_EQ(colony->tryClaimAnn(slot), AntColonyBpp9000T::AnnClaimOwned); + ASSERT_TRUE(colony->isAnnClaimHeld(slot)); + + EXPECT_EQ(AntColonyMaintenance::releaseInheritedClaims(*colony), 1u); + EXPECT_FALSE(colony->isAnnClaimHeld(slot)); + // Retryable, not merely unclaimed: a Busy here would be the hang the sweep exists to prevent. + EXPECT_EQ(colony->tryClaimAnn(slot), AntColonyBpp9000T::AnnClaimOwned); + colony->releaseAnnClaim(slot); + + EXPECT_EQ(AntColonyMaintenance::releaseInheritedClaims(*colony), 0u); +} + +// A rebuild starts from the parent's network, so a record whose parent has none cannot be taken yet. +TEST(TestAntColonyMaintenance, RebuildableOnlyOnceTheParentHasItsNetwork) +{ + AntColonyBpp9000T* colony = freshColony(); + ASSERT_NE(colony, nullptr) << "colony init failed; needs ~6.2 GB"; + + const m256i me = makeKey(42); + const long long parentIdx = commitRootChildWithoutAnn(colony, me, 3900, 0, 902); + ASSERT_NE(parentIdx, ANT_INVALID_INDEX); + SolutionRef parentRef; + parentRef.tick = 100000; + parentRef.solutionIndexInTick = 0; + const long long childIdx = commitChildWithoutAnn(colony, me, parentRef, 3800, 1, 903); + ASSERT_NE(childIdx, ANT_INVALID_INDEX); + + // The root is closed-form, so the parent is takeable immediately and the child is not. + EXPECT_TRUE(AntColonyMaintenance::isRebuildableNow(*colony, (unsigned int)parentIdx)); + EXPECT_FALSE(AntColonyMaintenance::isRebuildableNow(*colony, (unsigned int)childIdx)); + + AntColonyBpp9000T::Ann ann; + setMem(&ann, sizeof(ann), 0); + unsigned int annHash; + KangarooTwelve(&ann, sizeof(ann), &annHash, sizeof(annHash)); + ASSERT_EQ(colony->tryClaimAnn((unsigned int)parentIdx), AntColonyBpp9000T::AnnClaimOwned); + colony->publishAnn((unsigned int)parentIdx, ann, annHash); + + // Materialised records are done, and the level below is now unblocked. + EXPECT_FALSE(AntColonyMaintenance::isRebuildableNow(*colony, (unsigned int)parentIdx)); + EXPECT_TRUE(AntColonyMaintenance::isRebuildableNow(*colony, (unsigned int)childIdx)); +} + +// Two rebuilders must not walk the same record: the claim is what keeps the second one moving on. +TEST(TestAntColonyMaintenance, AClaimedRecordIsNotOfferedAgain) +{ + AntColonyBpp9000T* colony = freshColony(); + ASSERT_NE(colony, nullptr) << "colony init failed; needs ~6.2 GB"; + + const m256i me = makeKey(43); + const long long idx = commitRootChildWithoutAnn(colony, me, 3800, 0, 904); + ASSERT_NE(idx, ANT_INVALID_INDEX); + + EXPECT_TRUE(AntColonyMaintenance::isRebuildableNow(*colony, (unsigned int)idx)); + ASSERT_EQ(colony->tryClaimAnn((unsigned int)idx), AntColonyBpp9000T::AnnClaimOwned); + EXPECT_FALSE(AntColonyMaintenance::isRebuildableNow(*colony, (unsigned int)idx)); + + colony->releaseAnnClaim((unsigned int)idx); + EXPECT_TRUE(AntColonyMaintenance::isRebuildableNow(*colony, (unsigned int)idx)); +} From 1008d767194c532fac5ec6e786952474c71c34ee Mon Sep 17 00:00:00 2001 From: feiyu Date: Thu, 27 Aug 2026 17:50:55 +0700 Subject: [PATCH 02/21] ant walker: format to 160-column style --- src/extensions/ant_walker_client.h | 76 ++++++++++-------------------- src/extensions/ant_walker_worker.h | 27 ++++------- src/extensions/supervisor_shim.h | 3 +- src/qubic.cpp | 10 ++-- test/ant_colony.cpp | 5 +- 5 files changed, 41 insertions(+), 80 deletions(-) diff --git a/src/extensions/ant_walker_client.h b/src/extensions/ant_walker_client.h index fd1db01a..7ab98a61 100644 --- a/src/extensions/ant_walker_client.h +++ b/src/extensions/ant_walker_client.h @@ -124,8 +124,7 @@ inline const char* linkName(LinkState state) inline long long nowMs() { - return std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count(); + return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); } inline void logLine(const char* format, ...) @@ -393,8 +392,7 @@ inline bool dispatchOne() if (!job.isRoot) { const long long parentIndex = gAntColony.findIndexBySolutionRef(record->parentRef); - const AntSolutionRecord* parentRecord = - (parentIndex == ANT_INVALID_INDEX) ? nullptr : gAntColony.recordAt(parentIndex); + const AntSolutionRecord* parentRecord = (parentIndex == ANT_INVALID_INDEX) ? nullptr : gAntColony.recordAt(parentIndex); AntColonyBpp9000T::Ann parentAnn; if (parentRecord == nullptr || !gAntColony.annOfNonRoot(*parentRecord, parentAnn)) { @@ -417,8 +415,7 @@ inline bool dispatchOne() gState.jobsSent.fetch_add(1, std::memory_order_relaxed); if (gState.debug) { - logLine("job %llu record %u depth %u sent", (unsigned long long)job.jobId, index, - (unsigned)record->depth); + logLine("job %llu record %u depth %u sent", (unsigned long long)job.jobId, index, (unsigned)record->depth); } return true; } @@ -460,8 +457,7 @@ inline void noteDisagreement(unsigned int index) gState.disagreementStreak, (unsigned int)gState.rolledBackCandidates.size()); gState.rolledBackCandidates.clear(); gState.disagreementStreak = 0; - gState.suspectWalkerPid.store(gState.walkerPid.load(std::memory_order_acquire), - std::memory_order_release); + gState.suspectWalkerPid.store(gState.walkerPid.load(std::memory_order_acquire), std::memory_order_release); dropLink("walker disagrees on every job"); } @@ -482,12 +478,10 @@ inline void applyResult(const AntWalkProto::ResultPayload& result) const long long walkMs = nowMs() - job.sentAtMs; const unsigned long long previousEma = gState.walkMsEma.load(std::memory_order_acquire); - gState.walkMsEma.store(previousEma ? (previousEma * 3 + (unsigned long long)walkMs) / 4 - : (unsigned long long)walkMs, std::memory_order_release); + gState.walkMsEma.store(previousEma ? (previousEma * 3 + (unsigned long long)walkMs) / 4 : (unsigned long long)walkMs, std::memory_order_release); gState.lastResultAtMs.store((unsigned long long)nowMs(), std::memory_order_release); - if (result.epochId != gState.seedGeneration.load(std::memory_order_acquire) - || result.status == AntWalkProto::ResultStaleEpoch) + if (result.epochId != gState.seedGeneration.load(std::memory_order_acquire) || result.status == AntWalkProto::ResultStaleEpoch) { gAntColony.releaseAnnClaim(job.recordIndex); gState.staleDropped.fetch_add(1, std::memory_order_relaxed); @@ -506,8 +500,7 @@ inline void applyResult(const AntWalkProto::ResultPayload& result) if (result.status != AntWalkProto::ResultOk || result.score != record->score) { gAntColony.releaseAnnClaim(job.recordIndex); - logLine("record %u walked %u != accepted %u, marked failed", job.recordIndex, - result.score, record->score); + logLine("record %u walked %u != accepted %u, marked failed", job.recordIndex, result.score, record->score); noteDisagreement(job.recordIndex); return; } @@ -520,15 +513,13 @@ inline void applyResult(const AntWalkProto::ResultPayload& result) gAntColony.publishAnn(job.recordIndex, childAnn, annHash); // The same cache every scoring path consults, so a later strict replay of this solution is a // lookup rather than another walk. - const AntColonyBpp9000T::ReplayKey replayKey = - makeAntReplayKey(record->pubkey, record->nonce, record->parentRef, job.anchorDigest); + const AntColonyBpp9000T::ReplayKey replayKey = makeAntReplayKey(record->pubkey, record->nonce, record->parentRef, job.anchorDigest); gAntColony.putReplayScore(replayKey, result.score, childAnn); gState.materialised.fetch_add(1, std::memory_order_relaxed); noteSuccess(); if (gState.debug) { - logLine("job %llu record %u score %u in %lld ms", (unsigned long long)result.jobId, - job.recordIndex, result.score, walkMs); + logLine("job %llu record %u score %u in %lld ms", (unsigned long long)result.jobId, job.recordIndex, result.score, walkMs); } } @@ -552,8 +543,7 @@ inline void checkDeadlines() gAntColony.releaseAnnClaim(index); gState.deadlineExpiries.fetch_add(1, std::memory_order_relaxed); // A missing result says nothing about the record, so the bitmap is left alone. - logLine("job %llu record %u no result in %lld ms, claim released, not marked", - jobId, index, deadlineMs); + logLine("job %llu record %u no result in %lld ms, claim released, not marked", jobId, index, deadlineMs); gState.deadlineStreak++; } gState.inFlightCount.store((unsigned int)gState.inFlight.size(), std::memory_order_release); @@ -585,8 +575,7 @@ inline void serveQuiesce() gState.failedCount.store(0, std::memory_order_release); gState.quiesceAcknowledged.store(true, std::memory_order_release); - while (gState.quiesceRequested.load(std::memory_order_acquire) - && !gState.stopping.load(std::memory_order_acquire)) + while (gState.quiesceRequested.load(std::memory_order_acquire) && !gState.stopping.load(std::memory_order_acquire)) { std::this_thread::sleep_for(std::chrono::milliseconds(5)); } @@ -633,15 +622,13 @@ inline bool readOneFrame(int fd, unsigned char* payload) } if (ready.status != AntWalkProto::ReadyOk) { - logLine("handshake refused by the walker (status %u) - not retrying until it changes", - ready.status); + logLine("handshake refused by the walker (status %u) - not retrying until it changes", ready.status); dropLink("handshake refused"); return false; } // A ready frame also answers the hello resent when the mining seed rotates, which is a // re-seed of a link that never dropped rather than a new connection. - const bool wasReady = - (LinkState)gState.link.load(std::memory_order_acquire) == LinkState::Ready; + const bool wasReady = (LinkState)gState.link.load(std::memory_order_acquire) == LinkState::Ready; gState.threadCount = ready.threadCount; gState.walkerPid.store((int)ready.walkerPid, std::memory_order_release); gState.link.store((int)LinkState::Ready, std::memory_order_release); @@ -652,8 +639,7 @@ inline bool readOneFrame(int fd, unsigned char* payload) else { gState.reconnects.fetch_add(1, std::memory_order_relaxed); - logLine("sidecar connected, pid %u, %u threads, epoch %u", ready.walkerPid, - ready.threadCount, ready.epochId); + logLine("sidecar connected, pid %u, %u threads, epoch %u", ready.walkerPid, ready.threadCount, ready.epochId); } } else if (header.type == AntWalkProto::MsgResult) @@ -673,12 +659,9 @@ inline bool readOneFrame(int fd, unsigned char* payload) inline void heartbeat() { logLine("backlog %llu, done %llu, failed %llu, inflight %u/%u, walk avg %llu ms, link %s", - (unsigned long long)gState.backlog.load(std::memory_order_acquire), - (unsigned long long)gState.materialised.load(std::memory_order_acquire), - (unsigned long long)gState.failedCount.load(std::memory_order_acquire), - gState.inFlightCount.load(std::memory_order_acquire), gState.threadCount, - (unsigned long long)gState.walkMsEma.load(std::memory_order_acquire), - linkName((LinkState)gState.link.load(std::memory_order_acquire))); + (unsigned long long)gState.backlog.load(std::memory_order_acquire), (unsigned long long)gState.materialised.load(std::memory_order_acquire), + (unsigned long long)gState.failedCount.load(std::memory_order_acquire), gState.inFlightCount.load(std::memory_order_acquire), gState.threadCount, + (unsigned long long)gState.walkMsEma.load(std::memory_order_acquire), linkName((LinkState)gState.link.load(std::memory_order_acquire))); } inline void dispatcherLoop() @@ -731,8 +714,7 @@ inline void dispatcherLoop() continue; } - if (link == LinkState::Handshaking - && nowMs() - gState.handshakeStartedAtMs > HANDSHAKE_DEADLINE_MS) + if (link == LinkState::Handshaking && nowMs() - gState.handshakeStartedAtMs > HANDSHAKE_DEADLINE_MS) { dropLink("no ready frame within the handshake deadline"); continue; @@ -771,8 +753,7 @@ inline void dispatcherLoop() } const unsigned int generation = gState.seedGeneration.load(std::memory_order_acquire); - if (generation != gState.helloGeneration - && (LinkState)gState.link.load(std::memory_order_acquire) == LinkState::Ready) + if (generation != gState.helloGeneration && (LinkState)gState.link.load(std::memory_order_acquire) == LinkState::Ready) { const int helloFd = gState.fd.load(std::memory_order_acquire); if (helloFd < 0 || !sendHello(helloFd)) @@ -906,19 +887,12 @@ inline std::string statsJson() "\"inflight\":%u,\"backlog\":%llu,\"materialised\":%llu,\"failed\":%llu," "\"jobsSent\":%llu,\"disagreements\":%llu,\"deadlineExpiries\":%llu," "\"staleDropped\":%llu,\"reconnects\":%llu,\"walkAvgMs\":%llu,\"epochId\":%u}", - isEnabled() ? "true" : "false", - linkName((LinkState)gState.link.load(std::memory_order_acquire)), - gState.socketPath.c_str(), gState.threadCount, - gState.inFlightCount.load(std::memory_order_acquire), - (unsigned long long)gState.backlog.load(std::memory_order_acquire), - (unsigned long long)gState.materialised.load(std::memory_order_acquire), - (unsigned long long)gState.failedCount.load(std::memory_order_acquire), - (unsigned long long)gState.jobsSent.load(std::memory_order_acquire), - (unsigned long long)gState.disagreements.load(std::memory_order_acquire), - (unsigned long long)gState.deadlineExpiries.load(std::memory_order_acquire), - (unsigned long long)gState.staleDropped.load(std::memory_order_acquire), - (unsigned long long)gState.reconnects.load(std::memory_order_acquire), - (unsigned long long)gState.walkMsEma.load(std::memory_order_acquire), + isEnabled() ? "true" : "false", linkName((LinkState)gState.link.load(std::memory_order_acquire)), gState.socketPath.c_str(), gState.threadCount, + gState.inFlightCount.load(std::memory_order_acquire), (unsigned long long)gState.backlog.load(std::memory_order_acquire), + (unsigned long long)gState.materialised.load(std::memory_order_acquire), (unsigned long long)gState.failedCount.load(std::memory_order_acquire), + (unsigned long long)gState.jobsSent.load(std::memory_order_acquire), (unsigned long long)gState.disagreements.load(std::memory_order_acquire), + (unsigned long long)gState.deadlineExpiries.load(std::memory_order_acquire), (unsigned long long)gState.staleDropped.load(std::memory_order_acquire), + (unsigned long long)gState.reconnects.load(std::memory_order_acquire), (unsigned long long)gState.walkMsEma.load(std::memory_order_acquire), gState.seedGeneration.load(std::memory_order_acquire)); return std::string(buffer); } diff --git a/src/extensions/ant_walker_worker.h b/src/extensions/ant_walker_worker.h index d449850c..fc8cecfd 100644 --- a/src/extensions/ant_walker_worker.h +++ b/src/extensions/ant_walker_worker.h @@ -148,8 +148,7 @@ bool loadEmbeddedTask(score_engine::ScoreBpp9000T& engine) const unsigned int population = (unsigned int)BPP9000_POPULATION_THRESHOLD; const unsigned int neighbors = (unsigned int)BPP9000_NUMBER_OF_NEIGHBORS; - const unsigned long long topologyBytes = - score_task_file::topologyBytes(inputTrits, outputTrits, population, neighbors); + const unsigned long long topologyBytes = score_task_file::topologyBytes(inputTrits, outputTrits, population, neighbors); const unsigned char* topologyBlock = BPP9000_TASK_BYTES + sizeof(score_task_file::TaskFileHeader); const unsigned char* dataBlock = topologyBlock + topologyBytes; @@ -179,8 +178,7 @@ WorkerPool gPoolOfWorkers; void runWorker(unsigned int workerIndex) { - score_engine::ScoreBpp9000T* engine = (score_engine::ScoreBpp9000T*)aligned_alloc(64, - (sizeof(score_engine::ScoreBpp9000T) + 63) / 64 * 64); + score_engine::ScoreBpp9000T* engine = (score_engine::ScoreBpp9000T*)aligned_alloc(64, (sizeof(score_engine::ScoreBpp9000T) + 63) / 64 * 64); if (engine == nullptr || !loadEmbeddedTask(*engine)) { fprintf(stderr, "[ant-walker] worker %u could not load the embedded task\n", workerIndex); @@ -195,8 +193,7 @@ void runWorker(unsigned int workerIndex) AntWalkProto::JobPayload job; { std::unique_lock lock(gPoolOfWorkers.queueMutex); - gPoolOfWorkers.queueSignal.wait(lock, [] - { + gPoolOfWorkers.queueSignal.wait(lock, [] { return gPoolOfWorkers.stopping.load(std::memory_order_acquire) || !gPoolOfWorkers.queue.empty(); }); @@ -236,8 +233,7 @@ void runWorker(unsigned int workerIndex) parentAnn = (const Ann*)job.parentAnn; } - const unsigned int score = engine->computeScoreFromParent( - *parentAnn, job.pubkey, job.nonce, job.anchorDigest, gPool); + const unsigned int score = engine->computeScoreFromParent(*parentAnn, job.pubkey, job.nonce, job.anchorDigest, gPool); if (score == score_engine::INVALID_SCORE_VALUE) { result.status = AntWalkProto::ResultUnscorable; @@ -306,15 +302,13 @@ unsigned int validateHello(const AntWalkProto::HelloPayload& hello) { return AntWalkProto::ReadyVersionMismatch; } - if (memcmp(hello.topologyHash, BPP9000_TOPOLOGY_HASH, 32) != 0 - || memcmp(hello.dataHash, BPP9000_DATA_HASH, 32) != 0) + if (memcmp(hello.topologyHash, BPP9000_TOPOLOGY_HASH, 32) != 0 || memcmp(hello.dataHash, BPP9000_DATA_HASH, 32) != 0) { return AntWalkProto::ReadyTaskMismatch; } if (hello.configHash != AntWalkProto::CONFIG_HASH) { - fprintf(stderr, "[ant-walker] scorer config %08x does not match the node's %08x\n", - AntWalkProto::CONFIG_HASH, hello.configHash); + fprintf(stderr, "[ant-walker] scorer config %08x does not match the node's %08x\n", AntWalkProto::CONFIG_HASH, hello.configHash); fflush(stderr); return AntWalkProto::ReadyConfigMismatch; } @@ -349,8 +343,7 @@ void serveConnection(int fd, unsigned int threadCount) { continue; } - fprintf(stderr, "[ant-walker] no traffic for %d ms and nothing in flight, dropping the connection\n", - NO_TRAFFIC_TIMEOUT_MS); + fprintf(stderr, "[ant-walker] no traffic for %d ms and nothing in flight, dropping the connection\n", NO_TRAFFIC_TIMEOUT_MS); fflush(stderr); break; } @@ -362,8 +355,7 @@ void serveConnection(int fd, unsigned int threadCount) } if (header.magic != AntWalkProto::MAGIC || header.payloadSize > sizeof(payload)) { - fprintf(stderr, "[ant-walker] bad frame (magic %08x size %u), dropping the connection\n", - header.magic, header.payloadSize); + fprintf(stderr, "[ant-walker] bad frame (magic %08x size %u), dropping the connection\n", header.magic, header.payloadSize); fflush(stderr); break; } @@ -551,8 +543,7 @@ inline int run(int argc, const char* argv[]) { gPoolOfWorkers.threads.emplace_back(runWorker, i); } - fprintf(stderr, "[ant-walker] listening on %s, %u threads, pid %d\n", - options.socketPath.c_str(), options.threadCount, (int)getpid()); + fprintf(stderr, "[ant-walker] listening on %s, %u threads, pid %d\n", options.socketPath.c_str(), options.threadCount, (int)getpid()); fflush(stderr); while (!gPoolOfWorkers.stopping.load(std::memory_order_acquire)) diff --git a/src/extensions/supervisor_shim.h b/src/extensions/supervisor_shim.h index 0ff17260..c192c2c5 100644 --- a/src/extensions/supervisor_shim.h +++ b/src/extensions/supervisor_shim.h @@ -74,8 +74,7 @@ static pid_t shimForkAntWalker() char socketPath[128]; snprintf(socketPath, sizeof(socketPath), "/tmp/qubic-antwalk-%s.sock", gSidecarPort); - execl(self, "qubic-ant-walker", "--ant-walk-worker", "--socket", socketPath, - "--threads", gAntWalkerThreads, (char*)nullptr); + execl(self, "qubic-ant-walker", "--ant-walk-worker", "--socket", socketPath, "--threads", gAntWalkerThreads, (char*)nullptr); // Not fatal for the node: without a walker the backlog is simply paid on demand as before. fprintf(stderr, "[shim] could not exec the ant walker (%s), running without it\n", strerror(errno)); fflush(stderr); diff --git a/src/qubic.cpp b/src/qubic.cpp index 8ae46662..f99cf0b9 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -770,13 +770,11 @@ static bool materialiseOneAntRecord(unsigned long long processorNumber, unsigned // Same cache every other scoring path consults, so a rebuild never re-walks a score this node // already holds - including one restored from the on-disk replay cache. AntColonyBpp9000T::Ann& childAnn = gAntRebuildChildScratch[processorNumber]; - const AntColonyBpp9000T::ReplayKey replayKey = - makeAntReplayKey(rec->pubkey, rec->nonce, rec->parentRef, anchorDigest); + const AntColonyBpp9000T::ReplayKey replayKey = makeAntReplayKey(rec->pubkey, rec->nonce, rec->parentRef, anchorDigest); unsigned int rebuiltScore; if (!gAntColony.tryGetReplayScore(replayKey, rebuiltScore, childAnn)) { - rebuiltScore = score->computeAntChildScore(processorNumber, parentAnn, - rec->pubkey, rec->nonce, anchorDigest, childAnn); + rebuiltScore = score->computeAntChildScore(processorNumber, parentAnn, rec->pubkey, rec->nonce, anchorDigest, childAnn); gAntColony.putReplayScore(replayKey, rebuiltScore, childAnn); } @@ -10835,8 +10833,8 @@ static void bspForkPoint() { const char* offendingLock = forkCensusOffender(); ForkStats::onForkSkipped(ForkStats::CENSUS, (unsigned)system.tick, offendingLock ? offendingLock : "?"); - fprintf(stderr, "[FORK] census: non-BSP thread holds '%s' (%p) -> skip fork, run tick %u strict\n", - offendingLock ? offendingLock : "?", (const void*)forkCensusOffenderAddress(), (unsigned)system.tick); + fprintf(stderr, "[FORK] census: non-BSP thread holds '%s' (%p) -> skip fork, run tick %u strict\n", offendingLock ? offendingLock : "?", + (const void*)forkCensusOffenderAddress(), (unsigned)system.tick); fflush(stderr); quiescence.release(); tickFork::gChildPid = -1; diff --git a/test/ant_colony.cpp b/test/ant_colony.cpp index bd2f5434..17f0936a 100644 --- a/test/ant_colony.cpp +++ b/test/ant_colony.cpp @@ -1149,9 +1149,8 @@ TEST(TestAntColonyExport, BeginEpochClearsIt) } // A child of an existing node committed on trust: no network, the way an AUX node stores one. -static long long commitChildWithoutAnn(AntColonyBpp9000T* colony, const m256i& owner, - const SolutionRef& parentRef, unsigned int score, unsigned int txIdx, unsigned long long nonceSeed, - unsigned int tick = 100000) +static long long commitChildWithoutAnn(AntColonyBpp9000T* colony, const m256i& owner, const SolutionRef& parentRef, unsigned int score, + unsigned int txIdx, unsigned long long nonceSeed, unsigned int tick = 100000) { const AntSolutionRecord* parentRec = nullptr; if (colony->tryGetParent(parentRef, &parentRec) != ValidityResult::Valid) From ff7119328db3ebe674e60d7b5affe18853d77a9e Mon Sep 17 00:00:00 2001 From: feiyu Date: Thu, 27 Aug 2026 18:05:06 +0700 Subject: [PATCH 03/21] ant walker: trim comments --- src/extensions/ant_colony_maintenance.h | 12 ++-- src/extensions/ant_walker_client.h | 66 ++++++------------- src/extensions/ant_walker_proto.h | 14 ++-- src/extensions/ant_walker_worker.h | 29 +++----- .../http/controller/rpc_stats_controller.h | 3 +- src/extensions/supervisor_shim.h | 8 +-- src/platform/concurrency.h | 3 +- src/qubic.cpp | 6 +- test/ant_colony.cpp | 5 +- 9 files changed, 45 insertions(+), 101 deletions(-) diff --git a/src/extensions/ant_colony_maintenance.h b/src/extensions/ant_colony_maintenance.h index 60c2867b..e2073bfb 100644 --- a/src/extensions/ant_colony_maintenance.h +++ b/src/extensions/ant_colony_maintenance.h @@ -1,14 +1,11 @@ #pragma once -// Colony upkeep the node does outside consensus: dropping claims a fork child inherited, and -// deciding which records a background rebuild may take next. Both are pure functions of a colony so -// they can be exercised without a running node. +// Colony upkeep outside consensus. Pure functions of a colony, so they are testable without a node. namespace AntColonyMaintenance { -// An ant record claimed for a network rebuild sits at ANT_ANN_MATERIALISING until the claiming thread -// publishes or releases it. fork() clones only the calling thread, so a promoted child can inherit a -// claim whose owner never existed there, and ensureAntRecordAnn's waiter would spin on it forever. +// fork() clones only the calling thread, so a promoted child can inherit a claim with no owner and +// ensureAntRecordAnn's waiter would spin on it forever. inline unsigned int releaseInheritedClaims(AntColonyBpp9000T& colony) { unsigned int released = 0; @@ -24,8 +21,7 @@ inline unsigned int releaseInheritedClaims(AntColonyBpp9000T& colony) return released; } -// A record can be rebuilt only once its parent holds a network, so a scan in commit order - which is -// topological - walks each lineage from the bottom up and never repeats a level. +// A rebuild starts from the parent's network, so a record whose parent has none cannot be taken. inline bool isRebuildableNow(AntColonyBpp9000T& colony, unsigned int index) { if (colony.isAnnMaterialised(index) || colony.isAnnClaimHeld(index)) diff --git a/src/extensions/ant_walker_client.h b/src/extensions/ant_walker_client.h index 7ab98a61..fe2c09b2 100644 --- a/src/extensions/ant_walker_client.h +++ b/src/extensions/ant_walker_client.h @@ -1,14 +1,7 @@ #pragma once -// Node side of the ant walker sidecar: picks records whose network was never built, hands the walk -// to a separate process, and publishes the result after re-verifying it against the record. -// -// An AUX node trusting claimed scores commits every ant record unmaterialised, so the backlog grows -// all epoch and is paid back at ~one full walk per lineage level the first time a strict path needs -// a network (epoch end, a rollback replay, a MAIN switch). The walk cannot run on a node thread: it -// holds a score-engine lock for tens of seconds while a checkpoint window is only ~21 s, so the fork -// census would skip nearly every fork and force those ticks strict. Out of process the node holds no -// lock while the walk runs. +// Node side of the ant walker: picks records committed without a network, sends the walk to a +// separate process, publishes the result after re-verifying it against the record. #if defined(ANT_WALKER) && !defined(_WIN32) @@ -40,20 +33,18 @@ enum class LinkState Ready, }; -// A walker that scores differently than this node fails every job. Marking each one would poison -// good records permanently, so a run of them is read as a broken walker instead: the marks are -// rolled back and the link is dropped. +// A wrong walker fails every job, and marking each would poison good records permanently, so a +// streak is read as a broken walker instead. static constexpr unsigned int DISAGREEMENT_STREAK_LIMIT = 3; static constexpr unsigned int DEADLINE_STREAK_LIMIT = 3; -// A walk runs for minutes, and the walker answers PING throughout, so this is the "gone, not slow" -// threshold rather than an expected duration. +// "Gone", not "slow": a walk runs for minutes and the walker answers PING throughout. static constexpr long long MIN_JOB_DEADLINE_MS = 600'000; static constexpr long long PING_INTERVAL_MS = 10'000; static constexpr long long BACKOFF_MIN_MS = 100; static constexpr long long BACKOFF_MAX_MS = 30'000; static constexpr int POLL_SLICE_MS = 100; static constexpr long long HEARTBEAT_INTERVAL_MS = 60'000; -// Generous enough for a legitimate 512 MB pool derive, short enough that a wedged walker is visible. +// Covers a legitimate 512 MB pool derive without hiding a wedged walker. static constexpr long long HANDSHAKE_DEADLINE_MS = 120'000; struct InFlight @@ -61,8 +52,7 @@ struct InFlight unsigned long long jobId; unsigned int recordIndex; long long sentAtMs; - // Kept so the replay-cache key can be rebuilt when the result lands; re-resolving the anchor then - // could pick a different digest once the ring has moved on. + // Kept for the replay key: re-resolving the anchor later could pick a different digest. m256i anchorDigest; }; @@ -144,9 +134,7 @@ inline bool isEnabled() } // ── failed-record bitmap ──────────────────────────────────────────────────────────────────────── -// Node-local scheduling state only: it decides what the background dispatcher tries next. The -// on-demand rebuild path ignores it and still walks, because that runs when consensus needs the -// network rather than when spare capacity allows it. +// Scheduling only: the on-demand rebuild path ignores this and still walks. inline void ensureFailedBits() { @@ -243,8 +231,7 @@ inline bool sendFrame(int fd, unsigned int type, const void* payload, unsigned i // ── link ──────────────────────────────────────────────────────────────────────────────────────── -// Every exit from Ready goes through here, so no claim is ever left held behind a connection that no -// longer exists - the on-demand rebuild path would otherwise wait on it forever. +// Releases every in-flight claim: the on-demand path would otherwise wait on a dead connection. inline void dropLink(const char* reason) { const int fd = gState.fd.exchange(-1); @@ -314,8 +301,7 @@ inline bool tryConnect() // ── selection ─────────────────────────────────────────────────────────────────────────────────── -// Commit order is topological, so a forward scan that only takes records whose parent already has a -// network walks each lineage from the bottom up and never repeats a level. +// Commit order is topological, so this walks each lineage bottom-up without repeating a level. inline bool selectNextRecord(unsigned int& outIndex) { const unsigned int recordCount = gAntColony.solutionCount(); @@ -338,8 +324,7 @@ inline bool selectNextRecord(unsigned int& outIndex) return false; } -// Counting the whole backlog is a full pass over the records, so it runs on the heartbeat rather -// than on every dispatch: at mainnet's 2^23 records that scan is not something to repeat per job. +// A full record scan (2^23 on mainnet), so it runs on the heartbeat rather than per dispatch. inline void refreshBacklog() { const unsigned int recordCount = gAntColony.solutionCount(); @@ -437,8 +422,6 @@ inline bool takeInFlight(unsigned long long jobId, InFlight& out) return false; } -// A walker that scores this node's records wrongly fails all of them, so a streak is treated as a -// broken walker rather than broken records: the marks it caused are undone and the link is dropped. inline void noteDisagreement(unsigned int index) { markFailed(index); @@ -511,8 +494,7 @@ inline void applyResult(const AntWalkProto::ResultPayload& result) KangarooTwelve(&childAnn, sizeof(childAnn), &annHash, sizeof(annHash)); gAntColony.publishAnn(job.recordIndex, childAnn, annHash); - // The same cache every scoring path consults, so a later strict replay of this solution is a - // lookup rather than another walk. + // The cache every scoring path consults, so a strict replay of this solution is a lookup. const AntColonyBpp9000T::ReplayKey replayKey = makeAntReplayKey(record->pubkey, record->nonce, record->parentRef, job.anchorDigest); gAntColony.putReplayScore(replayKey, result.score, childAnn); gState.materialised.fetch_add(1, std::memory_order_relaxed); @@ -557,8 +539,7 @@ inline void checkDeadlines() // ── dispatcher ────────────────────────────────────────────────────────────────────────────────── -// The colony is about to be reseeded, so every index in flight is about to mean something else. -// Claims are dropped and the scan state cleared before the reset is allowed to proceed. +// Reseeding invalidates every index in flight, so claims and scan state go before it proceeds. inline void serveQuiesce() { for (const InFlight& job : gState.inFlight) @@ -626,8 +607,7 @@ inline bool readOneFrame(int fd, unsigned char* payload) dropLink("handshake refused"); return false; } - // A ready frame also answers the hello resent when the mining seed rotates, which is a - // re-seed of a link that never dropped rather than a new connection. + // Also answers the hello resent on a seed rotation, which is a re-seed, not a new link. const bool wasReady = (LinkState)gState.link.load(std::memory_order_acquire) == LinkState::Ready; gState.threadCount = ready.threadCount; gState.walkerPid.store((int)ready.walkerPid, std::memory_order_release); @@ -687,8 +667,7 @@ inline void dispatcherLoop() continue; } - // Before the first mining seed exists there is nothing to derive a pool from, and a walker - // seeded with zeros would disagree on every record. + // A walker seeded before the first mining seed exists would disagree on every record. if (score == nullptr || isZero(score->currentRandomSeed)) { std::this_thread::sleep_for(std::chrono::milliseconds(POLL_SLICE_MS)); @@ -743,8 +722,7 @@ inline void dispatcherLoop() checkDeadlines(); - // The queue is kept exactly as deep as the walker has threads: any more only lets a wedged - // walker hold claims the on-demand path may need. + // Deeper only lets a wedged walker hold claims the on-demand path may need. if ((LinkState)gState.link.load(std::memory_order_acquire) == LinkState::Ready) { while (gState.inFlight.size() < gState.threadCount && dispatchOne()) @@ -810,8 +788,7 @@ inline void stop() gState.dispatcher = nullptr; } -// The seed the walker derives its pool from changed, so any result still in flight was computed -// against the old one and must not be applied. +// The pool seed changed, so results still in flight were computed against the old one. inline void onEpochBegin() { gState.seedGeneration.fetch_add(1, std::memory_order_acq_rel); @@ -844,8 +821,7 @@ inline void quiesceEnd() gState.quiesceRequested.store(false, std::memory_order_release); } -// Runs in the fork child, where the dispatcher thread does not exist but its descriptor was -// inherited: two readers on one socket would interleave results. +// The fork child inherits the descriptor; two readers on one socket interleave results. inline void closeInheritedSocket() { const int fd = gState.fd.exchange(-1); @@ -856,13 +832,11 @@ inline void closeInheritedSocket() gState.link.store((int)LinkState::Disconnected, std::memory_order_release); } -// The promoted child owns the node now; its claims were swept by the promote path, so only the -// client's own view has to be rebuilt before a fresh dispatcher starts. +// The promote path swept the claims, so only the client's own view is rebuilt here. inline void restartAfterPromote() { closeInheritedSocket(); - // The parent's dispatcher thread did not come through fork(); only its handle did, so the handle - // is abandoned rather than joined or destroyed. + // Only the handle came through fork(), so it is abandoned rather than joined or destroyed. gState.dispatcher = nullptr; gState.inFlight.clear(); gState.inFlightCount.store(0, std::memory_order_release); diff --git a/src/extensions/ant_walker_proto.h b/src/extensions/ant_walker_proto.h index 6924f4a0..7f8f5e80 100644 --- a/src/extensions/ant_walker_proto.h +++ b/src/extensions/ant_walker_proto.h @@ -1,11 +1,7 @@ #pragma once -// Wire format shared by the node's walker client and the qubic-ant-walker sidecar. Fixed-size -// payloads only, so a frame is one read of a known length. -// -// The sidecar is a pure function of the job payload: it holds no node state and the node re-verifies -// every score against the record before publishing, so a wrong or stale sidecar can only waste its -// own CPU, never corrupt consensus. +// Wire format shared by the walker client and the walker process. Fixed-size payloads only, so a +// frame is one read of a known length. #include "score.h" #include "public_settings.h" @@ -15,12 +11,10 @@ namespace AntWalkProto static constexpr unsigned int MAGIC = 0x57544E41u; // "ANTW" static constexpr unsigned int VERSION = 1; -// Bumped past the 512 MB pool build, so a walker that predates a wire change is refused at HELLO. static constexpr unsigned int ANN_BYTES = (unsigned int)sizeof(score_engine::ScoreBpp9000T::ANN); -// Both binaries compile the same scorer headers, so the only way they can score a nonce differently -// is a build that disagrees on the parameters feeding it. Comparing this at the handshake catches -// that before a job runs, rather than as a walker whose every result the node rejects. +// Both sides compile the same scorer, so only a build disagreeing on these params scores +// differently. Compared at the handshake, before a job runs. constexpr unsigned int mixConfig(unsigned int accumulated, unsigned long long value) { for (unsigned int byteIndex = 0; byteIndex < 8; byteIndex++) diff --git a/src/extensions/ant_walker_worker.h b/src/extensions/ant_walker_worker.h index fc8cecfd..a191ef4d 100644 --- a/src/extensions/ant_walker_worker.h +++ b/src/extensions/ant_walker_worker.h @@ -1,13 +1,7 @@ #pragma once -// The ant score walk, run in a separate process re-exec'd from this same binary (the shim passes -// --ant-walk-worker). A walk holds a score-engine lock for tens of seconds while a checkpoint window -// is only ~21 s, so on a node thread the fork census would skip nearly every fork and force those -// ticks strict. Here the node only sends a job and applies a verified result. -// -// Stateless: the pool is re-derived from the seed the node sends and the task is the blob compiled -// into this binary. Nothing about a node's colony lives here, and the node re-verifies every score -// against the record before publishing it. +// The ant score walk, in a process re-exec'd from this binary (--ant-walk-worker): on a node thread +// it would hold the score-engine lock across nearly every fork point. Stateless, holds no node state. #if defined(ANT_WALKER) && !defined(_WIN32) @@ -84,8 +78,7 @@ bool writeFully(int fd, const void* buffer, size_t size) return true; } -// Waits for readable with a bounded total timeout, so a peer that vanished without an EOF cannot -// leave this process parked forever on a connection that will never speak again. +// Bounded, so a peer that vanished without an EOF cannot park this process forever. bool waitReadable(int fd, int timeoutMs, const std::atomic& stop) { int waitedMs = 0; @@ -247,8 +240,7 @@ void runWorker(unsigned int workerIndex) } } - // A walk cannot be aborted mid-flight, so one that outlived its connection is finished and - // then dropped rather than written to whatever now owns that descriptor. + // A walk cannot be aborted, so one that outlived its connection finishes and is dropped. if (!gPoolOfWorkers.connected.load(std::memory_order_acquire)) { continue; @@ -337,8 +329,7 @@ void serveConnection(int fd, unsigned int threadCount) { if (!waitReadable(fd, NO_TRAFFIC_TIMEOUT_MS, gPoolOfWorkers.stopping)) { - // A single walk runs far longer than this timeout, so silence only means a dead peer - // when there is nothing outstanding to answer with. + // A walk outlasts this timeout, so silence only means a dead peer when nothing is queued. if (hasWorkOutstanding()) { continue; @@ -425,8 +416,7 @@ void serveConnection(int fd, unsigned int threadCount) gPoolOfWorkers.queue.clear(); } { - // Held until every worker that might still write is out of writeFully, so the descriptor is - // never closed under one of them. + // Held until no worker can still be inside writeFully on this descriptor. std::lock_guard writeGuard(gPoolOfWorkers.writeMutex); gPoolOfWorkers.connectionFd.store(-1, std::memory_order_release); } @@ -435,9 +425,7 @@ void serveConnection(int fd, unsigned int threadCount) // ── listener ──────────────────────────────────────────────────────────────────────────────────── -// A live server on this path means this process would silently compete with it for the node's jobs, -// which is how an orphaned sidecar ends up answering for a running one. Only a socket nothing answers -// on is stale enough to replace. +// Competing with a live server is how an orphaned sidecar ends up answering for a running one. bool socketPathIsServed(const char* path) { const int probeFd = socket(AF_UNIX, SOCK_STREAM, 0); @@ -500,8 +488,7 @@ bool parseOptions(int argc, const char* argv[], Options& options) return !options.socketPath.empty() && options.threadCount > 0; } -// The shim re-execs this binary with the worker flag rather than shipping a second one; the node -// half of main() must not run here, so this is checked before any node setup. +// Checked before any node setup: the node half of main() must not run in the walker. inline bool requested(int argc, const char* argv[]) { for (int i = 1; i < argc; i++) diff --git a/src/extensions/http/controller/rpc_stats_controller.h b/src/extensions/http/controller/rpc_stats_controller.h index 0535f76b..cfa7be22 100644 --- a/src/extensions/http/controller/rpc_stats_controller.h +++ b/src/extensions/http/controller/rpc_stats_controller.h @@ -140,8 +140,7 @@ RPC_ROUTE("GET", "/v1/fork-stats") return r; } -// Ant walker sidecar health: link state, backlog and the counters that tell a walker chewing through -// work from one that is up but delivering nothing. +// Ant walker health: separates a walker chewing through work from one up but delivering nothing. RPC_ROUTE("GET", "/v1/ant-walker") { (void)req; diff --git a/src/extensions/supervisor_shim.h b/src/extensions/supervisor_shim.h index c192c2c5..51950105 100644 --- a/src/extensions/supervisor_shim.h +++ b/src/extensions/supervisor_shim.h @@ -50,9 +50,8 @@ static pid_t shimForkSidecar() #endif } -// Re-exec self as the ant walker, a sibling of the node: the ant score walk runs there so the node -// never holds a score-engine lock across a checkpoint fork point. Spawned here rather than by the -// node so it outlives a rollback promotion. +// Re-exec self as the ant walker, a sibling of the node. Spawned here, not by the node, so it +// outlives a rollback promotion. static pid_t shimForkAntWalker() { if (std::atoi(gAntWalkerThreads) <= 0) @@ -163,8 +162,7 @@ static inline void runUnderSupervisor(int argc, const char** argv) if (antWalker > 0 && reapedPid == antWalker) { sleep(1); // a squatted socket would hot-loop the respawn - // A missing or unrunnable walker binary would otherwise respawn once a second forever; - // the node works without one, it just pays the walks on demand. + // An unrunnable walker would otherwise respawn once a second forever. if (++antWalkerRestarts > 5) { fprintf(stderr, "[shim] ant walker died %d times, leaving it down\n", antWalkerRestarts); diff --git a/src/platform/concurrency.h b/src/platform/concurrency.h index 980ece2b..8aa653be 100644 --- a/src/platform/concurrency.h +++ b/src/platform/concurrency.h @@ -40,8 +40,7 @@ namespace ForkCensus { std::atomic depth{ 0 }; std::atomic what{ nullptr }; - // Every LockGuard-based lock reports the same `what` (the macro expands inside LockGuard's - // constructor), so the address is the only thing that says which lock blocked a fork. + // Every LockGuard lock reports the same `what`, so only the address names the offender. std::atomic where{ nullptr }; std::atomic live{ 0 }; // 0 = free/reusable, 1 = owned by a live thread }; diff --git a/src/qubic.cpp b/src/qubic.cpp index f99cf0b9..77bc7bc0 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -767,8 +767,7 @@ static bool materialiseOneAntRecord(unsigned long long processorNumber, unsigned return false; } - // Same cache every other scoring path consults, so a rebuild never re-walks a score this node - // already holds - including one restored from the on-disk replay cache. + // The cache every other scoring path consults, so a rebuild never re-walks a score we hold. AntColonyBpp9000T::Ann& childAnn = gAntRebuildChildScratch[processorNumber]; const AntColonyBpp9000T::ReplayKey replayKey = makeAntReplayKey(rec->pubkey, rec->nonce, rec->parentRef, anchorDigest); unsigned int rebuiltScore; @@ -12423,8 +12422,7 @@ int main(int argc, const char* argv[]) return tickStorageScan::scan(); } #endif - // Before any node setup: this process is the walker sidecar, not a node. It leaves via _exit so - // the node's static destructors never run against globals this process never initialised. + // The walker, not a node. _exit so static destructors never run against uninitialised globals. if (AntWalkerWorker::requested(argc, argv)) { _exit(AntWalkerWorker::run(argc, argv)); diff --git a/test/ant_colony.cpp b/test/ant_colony.cpp index 17f0936a..0db4fc19 100644 --- a/test/ant_colony.cpp +++ b/test/ant_colony.cpp @@ -1175,8 +1175,7 @@ static long long commitChildWithoutAnn(AntColonyBpp9000T* colony, const m256i& o return landsAt; } -// fork() clones one thread, so a claim held at fork time has no owner in the child and the on-demand -// waiter would spin on it forever. +// A claim held at fork time has no owner in the child, and the on-demand waiter would spin on it. TEST(TestAntColonyMaintenance, PromoteReleasesAnInheritedClaim) { AntColonyBpp9000T* colony = freshColony(); @@ -1192,7 +1191,7 @@ TEST(TestAntColonyMaintenance, PromoteReleasesAnInheritedClaim) EXPECT_EQ(AntColonyMaintenance::releaseInheritedClaims(*colony), 1u); EXPECT_FALSE(colony->isAnnClaimHeld(slot)); - // Retryable, not merely unclaimed: a Busy here would be the hang the sweep exists to prevent. + // Retryable, not merely unclaimed: a Busy here is the hang the sweep exists to prevent. EXPECT_EQ(colony->tryClaimAnn(slot), AntColonyBpp9000T::AnnClaimOwned); colony->releaseAnnClaim(slot); From b0542a1d99bed285988b2adb1f9e88f9987c65ce Mon Sep 17 00:00:00 2001 From: feiyu Date: Fri, 28 Aug 2026 21:24:18 +0700 Subject: [PATCH 04/21] ant walker: skip jobs already memoised, let the tick reclaim a stuck walk --- src/extensions/ant_walker_client.h | 118 +++++++++++++++++++++++++++-- src/qubic.cpp | 40 +++++++--- 2 files changed, 142 insertions(+), 16 deletions(-) diff --git a/src/extensions/ant_walker_client.h b/src/extensions/ant_walker_client.h index fe2c09b2..b4dd2248 100644 --- a/src/extensions/ant_walker_client.h +++ b/src/extensions/ant_walker_client.h @@ -46,6 +46,11 @@ static constexpr int POLL_SLICE_MS = 100; static constexpr long long HEARTBEAT_INTERVAL_MS = 60'000; // Covers a legitimate 512 MB pool derive without hiding a wedged walker. static constexpr long long HANDSHAKE_DEADLINE_MS = 120'000; +// A record the tick is blocked on is handed back, but only once the job is old enough to be stuck: +// yanking a healthy walk throws its progress away and the node pays the whole walk again. +static constexpr long long PREEMPT_MIN_AGE_MS = 180'000; +static constexpr long long PREEMPT_ACK_WAIT_MS = 500; +static constexpr unsigned int PREEMPT_NONE = 0xFFFFFFFFu; struct InFlight { @@ -70,10 +75,13 @@ struct State std::atomic seedGeneration{ 0 }; std::atomic walkerPid{ -1 }; std::atomic suspectWalkerPid{ -1 }; + std::atomic preemptRequest{ PREEMPT_NONE }; + std::atomic handedBack{ PREEMPT_NONE }; std::atomic jobsSent{ 0 }; std::atomic materialised{ 0 }; std::atomic memoHits{ 0 }; + std::atomic preempted{ 0 }; std::atomic disagreements{ 0 }; std::atomic deadlineExpiries{ 0 }; std::atomic staleDropped{ 0 }; @@ -313,6 +321,15 @@ inline bool selectNextRecord(unsigned int& outIndex) for (unsigned int scanned = 0; scanned < recordCount; scanned++) { const unsigned int index = (gState.cursor + scanned) % recordCount; + if (index == gState.handedBack.load(std::memory_order_acquire)) + { + // Held until the tick has actually taken it, so the walker cannot re-take what it gave up. + if (gAntColony.isAnnClaimHeld(index) || gAntColony.isAnnMaterialised(index)) + { + gState.handedBack.store(PREEMPT_NONE, std::memory_order_release); + } + continue; + } if (isFailed(index) || !AntColonyMaintenance::isRebuildableNow(gAntColony, index)) { continue; @@ -374,6 +391,32 @@ inline bool dispatchOne() } copyMem(job.anchorDigest, anchorDigest.m256i_u8, 32); + // Every other scoring path memoises its walk, so a score already held here costs no job at all. + const AntColonyBpp9000T::ReplayKey replayKey = makeAntReplayKey(record->pubkey, record->nonce, record->parentRef, anchorDigest); + AntColonyBpp9000T::Ann memoAnn; + unsigned int memoScore; + if (gAntColony.tryGetReplayScore(replayKey, memoScore, memoAnn)) + { + if (memoScore != record->score) + { + // The node's own cache disagreeing is the record's fault, not the walker's, so no streak. + gAntColony.releaseAnnClaim(index); + markFailed(index); + logLine("record %u memo %u != accepted %u, marked failed", index, memoScore, record->score); + return false; + } + unsigned int memoHash; + KangarooTwelve(&memoAnn, sizeof(memoAnn), &memoHash, sizeof(memoHash)); + gAntColony.publishAnn(index, memoAnn, memoHash); + gState.memoHits.fetch_add(1, std::memory_order_relaxed); + gState.materialised.fetch_add(1, std::memory_order_relaxed); + if (gState.debug) + { + logLine("record %u from the replay cache, no job", index); + } + return true; + } + if (!job.isRoot) { const long long parentIndex = gAntColony.findIndexBySolutionRef(record->parentRef); @@ -539,6 +582,37 @@ inline void checkDeadlines() // ── dispatcher ────────────────────────────────────────────────────────────────────────────────── +// Answers the on-demand path waiting on a record this link claimed. Only a job past the stale age is +// handed back: a healthy walk is worth waiting out, since taking it back discards all of its progress. +inline void servePreempt() +{ + const unsigned int index = gState.preemptRequest.load(std::memory_order_acquire); + if (index == PREEMPT_NONE) + { + return; + } + + const unsigned long long ema = gState.walkMsEma.load(std::memory_order_acquire); + const long long staleMs = (long long)(ema * 2) > PREEMPT_MIN_AGE_MS ? (long long)(ema * 2) : PREEMPT_MIN_AGE_MS; + for (size_t i = 0; i < gState.inFlight.size(); i++) + { + if (gState.inFlight[i].recordIndex != index || nowMs() - gState.inFlight[i].sentAtMs < staleMs) + { + continue; + } + const unsigned long long jobId = gState.inFlight[i].jobId; + const long long ageMs = nowMs() - gState.inFlight[i].sentAtMs; + gState.inFlight.erase(gState.inFlight.begin() + (long)i); + gState.inFlightCount.store((unsigned int)gState.inFlight.size(), std::memory_order_release); + gState.handedBack.store(index, std::memory_order_release); + gAntColony.releaseAnnClaim(index); + gState.preempted.fetch_add(1, std::memory_order_relaxed); + logLine("job %llu record %u handed back to the tick after %lld ms, its result will be dropped", jobId, index, ageMs); + break; + } + gState.preemptRequest.store(PREEMPT_NONE, std::memory_order_release); +} + // Reseeding invalidates every index in flight, so claims and scan state go before it proceeds. inline void serveQuiesce() { @@ -548,6 +622,8 @@ inline void serveQuiesce() } gState.inFlight.clear(); gState.inFlightCount.store(0, std::memory_order_release); + gState.preemptRequest.store(PREEMPT_NONE, std::memory_order_release); + gState.handedBack.store(PREEMPT_NONE, std::memory_order_release); gState.cursor = 0; gState.disagreementStreak = 0; gState.deadlineStreak = 0; @@ -638,10 +714,11 @@ inline bool readOneFrame(int fd, unsigned char* payload) inline void heartbeat() { - logLine("backlog %llu, done %llu, failed %llu, inflight %u/%u, walk avg %llu ms, link %s", + logLine("backlog %llu, done %llu (memo %llu), failed %llu, inflight %u/%u, walk avg %llu ms, link %s", (unsigned long long)gState.backlog.load(std::memory_order_acquire), (unsigned long long)gState.materialised.load(std::memory_order_acquire), - (unsigned long long)gState.failedCount.load(std::memory_order_acquire), gState.inFlightCount.load(std::memory_order_acquire), gState.threadCount, - (unsigned long long)gState.walkMsEma.load(std::memory_order_acquire), linkName((LinkState)gState.link.load(std::memory_order_acquire))); + (unsigned long long)gState.memoHits.load(std::memory_order_acquire), (unsigned long long)gState.failedCount.load(std::memory_order_acquire), + gState.inFlightCount.load(std::memory_order_acquire), gState.threadCount, (unsigned long long)gState.walkMsEma.load(std::memory_order_acquire), + linkName((LinkState)gState.link.load(std::memory_order_acquire))); } inline void dispatcherLoop() @@ -654,6 +731,8 @@ inline void dispatcherLoop() while (!gState.stopping.load(std::memory_order_acquire)) { + servePreempt(); + if (nowMs() >= nextHeartbeatAtMs) { nextHeartbeatAtMs = nowMs() + HEARTBEAT_INTERVAL_MS; @@ -821,6 +900,30 @@ inline void quiesceEnd() gState.quiesceRequested.store(false, std::memory_order_release); } +// Asks the dispatcher to hand back a record the on-demand path is stuck behind. The caller re-checks +// the claim afterwards: a claim held by another node processor is not the walker's to release. +inline void preemptClaim(unsigned int index) +{ + if (!isEnabled() || gState.inFlightCount.load(std::memory_order_acquire) == 0) + { + return; + } + unsigned int slot = PREEMPT_NONE; + if (!gState.preemptRequest.compare_exchange_strong(slot, index, std::memory_order_acq_rel)) + { + return; + } + + const long long deadlineMs = nowMs() + PREEMPT_ACK_WAIT_MS; + while (gState.preemptRequest.load(std::memory_order_acquire) == index && nowMs() < deadlineMs) + { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + // A dispatcher that never answered must not leave the slot taken for every later request. + slot = index; + gState.preemptRequest.compare_exchange_strong(slot, PREEMPT_NONE, std::memory_order_acq_rel); +} + // The fork child inherits the descriptor; two readers on one socket interleave results. inline void closeInheritedSocket() { @@ -840,6 +943,8 @@ inline void restartAfterPromote() gState.dispatcher = nullptr; gState.inFlight.clear(); gState.inFlightCount.store(0, std::memory_order_release); + gState.preemptRequest.store(PREEMPT_NONE, std::memory_order_release); + gState.handedBack.store(PREEMPT_NONE, std::memory_order_release); gState.quiesceRequested.store(false, std::memory_order_release); gState.quiesceAcknowledged.store(true, std::memory_order_release); gState.cursor = 0; @@ -855,18 +960,20 @@ inline void restartAfterPromote() inline std::string statsJson() { - char buffer[768]; + char buffer[896]; snprintf(buffer, sizeof(buffer), "{\"enabled\":%s,\"state\":\"%s\",\"socket\":\"%s\",\"threads\":%u," "\"inflight\":%u,\"backlog\":%llu,\"materialised\":%llu,\"failed\":%llu," "\"jobsSent\":%llu,\"disagreements\":%llu,\"deadlineExpiries\":%llu," - "\"staleDropped\":%llu,\"reconnects\":%llu,\"walkAvgMs\":%llu,\"epochId\":%u}", + "\"staleDropped\":%llu,\"reconnects\":%llu,\"walkAvgMs\":%llu," + "\"memoHits\":%llu,\"preempted\":%llu,\"epochId\":%u}", isEnabled() ? "true" : "false", linkName((LinkState)gState.link.load(std::memory_order_acquire)), gState.socketPath.c_str(), gState.threadCount, gState.inFlightCount.load(std::memory_order_acquire), (unsigned long long)gState.backlog.load(std::memory_order_acquire), (unsigned long long)gState.materialised.load(std::memory_order_acquire), (unsigned long long)gState.failedCount.load(std::memory_order_acquire), (unsigned long long)gState.jobsSent.load(std::memory_order_acquire), (unsigned long long)gState.disagreements.load(std::memory_order_acquire), (unsigned long long)gState.deadlineExpiries.load(std::memory_order_acquire), (unsigned long long)gState.staleDropped.load(std::memory_order_acquire), (unsigned long long)gState.reconnects.load(std::memory_order_acquire), (unsigned long long)gState.walkMsEma.load(std::memory_order_acquire), + (unsigned long long)gState.memoHits.load(std::memory_order_acquire), (unsigned long long)gState.preempted.load(std::memory_order_acquire), gState.seedGeneration.load(std::memory_order_acquire)); return std::string(buffer); } @@ -884,6 +991,7 @@ inline void stop() {} inline void onEpochBegin() {} inline void quiesceBegin() {} inline void quiesceEnd() {} +inline void preemptClaim(unsigned int) {} inline void closeInheritedSocket() {} inline void restartAfterPromote() {} inline std::string statsJson() { return std::string("{\"enabled\":false}"); } diff --git a/src/qubic.cpp b/src/qubic.cpp index 77bc7bc0..56b7050d 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -714,27 +714,45 @@ static bool getAntAnchorDigestForRebuild(unsigned int tick, m256i& out) return recomputeAntAnchorDigest(tick, out); } +namespace AntWalker +{ +inline void preemptClaim(unsigned int index); +} + +// A background walker job holds its claim for a whole walk, so a rebuild the tick needs would queue +// behind it. Asking repeatedly is free: the walker only hands back a job old enough to be stuck. +static constexpr unsigned int ANT_ANN_CLAIM_PREEMPT_POLLS = 250; + +// Waiting costs what the walk costs; walking it here as well would pay that twice. +static AntColonyBpp9000T::AnnClaim waitForAnnClaim(unsigned int idx) +{ + for (unsigned int polls = 1; gAntColony.isAnnClaimHeld(idx); polls++) + { + sleepMilliseconds(20); + if (polls % ANT_ANN_CLAIM_PREEMPT_POLLS == 0) + { + AntWalker::preemptClaim(idx); + } + } + return gAntColony.tryClaimAnn(idx); +} + // Rebuilds one record's network. Its parent must already have one, or be the root. static bool materialiseOneAntRecord(unsigned long long processorNumber, unsigned int idx) { - const AntColonyBpp9000T::AnnClaim claim = gAntColony.tryClaimAnn(idx); + AntColonyBpp9000T::AnnClaim claim = gAntColony.tryClaimAnn(idx); + while (claim == AntColonyBpp9000T::AnnClaimBusy) + { + claim = waitForAnnClaim(idx); + } if (claim == AntColonyBpp9000T::AnnClaimReady) { return true; } - if (claim == AntColonyBpp9000T::AnnClaimInvalid) + if (claim != AntColonyBpp9000T::AnnClaimOwned) { return false; } - if (claim == AntColonyBpp9000T::AnnClaimBusy) - { - // Waiting costs what the walk costs; walking it here as well would pay that twice. - while (gAntColony.isAnnClaimHeld(idx)) - { - sleepMilliseconds(20); - } - return gAntColony.isAnnMaterialised(idx); - } // Owned from here on, so every exit below either publishes or releases the claim. const unsigned long long rebuildStart = __rdtsc(); From 3b424e887e45f400358d4bc8085f0cd62700df0c Mon Sep 17 00:00:00 2001 From: cyber-pc <165458555+cyber-pc@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:58:28 +0700 Subject: [PATCH 05/21] HOTFIX: Set tick 77259626 empty --- src/public_settings.h | 2 +- src/qubic.cpp | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/public_settings.h b/src/public_settings.h index fa4760c6..d4920889 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -74,7 +74,7 @@ static_assert(AUTO_FORCE_NEXT_TICK_THRESHOLD* TARGET_TICK_DURATION >= PEER_REFRE #define VERSION_A 1 #define VERSION_B 302 -#define VERSION_C 0 +#define VERSION_C 1 // Epoch and initial tick for node startup #define EPOCH 228 diff --git a/src/qubic.cpp b/src/qubic.cpp index e6570f0a..27929295 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -1188,9 +1188,9 @@ static void processBroadcastTick(Peer* peer, RequestResponseHeader* header) } else { - // hot fix: only accept "empty" votes for stuck tick 73924308 + // hot fix: only accept "empty" votes for stuck tick 77259626 bool isOk = true; - if (request->tick.tick == 73924308) + if (request->tick.tick == 77259626) { // only accept zero transactionDigest if (!isZero(request->tick.transactionDigest)) @@ -6556,10 +6556,10 @@ static void tickProcessor(void*) tickDataSuits = true; } - // hot fix: force tick 73924308 to be empty - if (system.tick == 73924307) + // hot fix: force tick 77259626 to be empty + if (system.tick == 77259625) { - // ignore next tick (73924308) + // ignore next tick (77259626) targetNextTickDataDigest = m256i::zero(); targetNextTickDataDigestIsKnown = true; } From 447f9834c23547e0e5bf8425232509e42f51407d Mon Sep 17 00:00:00 2001 From: feiyu Date: Mon, 31 Aug 2026 21:31:31 +0700 Subject: [PATCH 06/21] trace the stamped contract index the trace snapshot was taken before hostServices.logBytes stamps the leading word, so every traced log reported contract index 0 while the log store recorded the real one. --- src/extensions/wasm/runtime/lhost_registry.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/extensions/wasm/runtime/lhost_registry.h b/src/extensions/wasm/runtime/lhost_registry.h index 67d37fab..0845245a 100644 --- a/src/extensions/wasm/runtime/lhost_registry.h +++ b/src/extensions/wasm/runtime/lhost_registry.h @@ -248,6 +248,8 @@ static void w_logBytes(wasm_exec_env_t execEnv, uint32_t contractIndex, uint32_t if (callContext && callContext->trace) { + // hostServices.logBytes stamps this word for the log store and zeroes it after; the trace must capture the stamped bytes. + *((unsigned int*)message) = contractIndex; recordLog((TraceEntry*)callContext->trace, (unsigned char)type, message, size); } From cc72f8cd719f016a2cce8574a8916750d2b23b46 Mon Sep 17 00:00:00 2001 From: feiyu Date: Mon, 31 Aug 2026 23:07:26 +0700 Subject: [PATCH 07/21] Add a cheat host row for development cheatcodes One opcode-dispatched lhost row instead of one row per cheat, so later cheatcodes cost no import and no ABI bump. Refusal is always a negative return, never a trap, so a client that outruns the node degrades loudly. CC_PRINT records against the debug trace only: no log id, no qLogger, no tick log range. Warp shifts what the contract observes while the node still commits the real tick, so it cannot move consensus. --- .../http/controller/rpc_live_controller.h | 13 ++ src/extensions/wasm/runtime/dispatch.h | 2 + src/extensions/wasm/runtime/host_services.h | 1 + src/extensions/wasm/runtime/lhost_registry.h | 40 ++++++ src/extensions/wasm/runtime/qpi_services.h | 134 +++++++++++++++++- src/extensions/wasm/runtime/trace.h | 25 ++++ src/extensions/wasm/sdk/lhost_imports.h | 1 + src/extensions/wasm/shared/abi_metadata.h | 5 +- src/extensions/wasm/shared/abi_types.h | 18 +++ 9 files changed, 235 insertions(+), 4 deletions(-) diff --git a/src/extensions/http/controller/rpc_live_controller.h b/src/extensions/http/controller/rpc_live_controller.h index 9530edad..c8fbbc53 100644 --- a/src/extensions/http/controller/rpc_live_controller.h +++ b/src/extensions/http/controller/rpc_live_controller.h @@ -715,6 +715,19 @@ RPC_ROUTE("GET", "/live/v1/debug-trace") logs.append(logEntry); } entry["logs"] = logs; + + Json::Value cheats(Json::arrayValue); + for (const auto& cheat : trace.cheats) + { + Json::Value cheatEntry; + cheatEntry["id"] = cheat.id; + cheatEntry["part"] = (unsigned int)cheat.part; + cheatEntry["size"] = cheat.size; + cheatEntry["value"] = (Json::UInt64)cheat.value; + cheatEntry["hex"] = cheat.hex; + cheats.append(cheatEntry); + } + entry["cheats"] = cheats; entries.append(entry); } json["entries"] = entries; diff --git a/src/extensions/wasm/runtime/dispatch.h b/src/extensions/wasm/runtime/dispatch.h index d55e0674..db745947 100644 --- a/src/extensions/wasm/runtime/dispatch.h +++ b/src/extensions/wasm/runtime/dispatch.h @@ -79,6 +79,7 @@ static CallContext createCallContext(const void* context, uint32_t arenaStart, u callContext.arenaStart = arenaStart; callContext.arenaTop = arenaStart; callContext.arenaLimit = arenaLimit; + clearCheatWarp(); return callContext; } @@ -88,6 +89,7 @@ static void bindJournal(CallContext& callContext, const EngineSlot& slot) callContext.journalBaseOffset = slot.journalBaseOffset; callContext.stateOffset = slot.stateOffset; callContext.journalHeader = slot.journalBaseOffset ? &slot.journalHeader : nullptr; + callContext.guestContextOffset = slot.contextOffset; } static void bindEnvironment(wasm_exec_env_t execEnv, CallContext& callContext) diff --git a/src/extensions/wasm/runtime/host_services.h b/src/extensions/wasm/runtime/host_services.h index 083daf21..5ef268f7 100644 --- a/src/extensions/wasm/runtime/host_services.h +++ b/src/extensions/wasm/runtime/host_services.h @@ -75,6 +75,7 @@ static HostServices hostServices = .liteInvokeProcedure = &invokeContractProcedure, .setShareholderProposal = &setShareholderProposal, .setShareholderVotes = &setShareholderVotes, + .cheat = &cheat, }; } // namespace Wasm::Runtime diff --git a/src/extensions/wasm/runtime/lhost_registry.h b/src/extensions/wasm/runtime/lhost_registry.h index 0845245a..03af8200 100644 --- a/src/extensions/wasm/runtime/lhost_registry.h +++ b/src/extensions/wasm/runtime/lhost_registry.h @@ -25,6 +25,8 @@ struct CallContext uint32_t journalBaseOffset = 0; uint32_t stateOffset = 0; const JournalHeader* journalHeader = nullptr; + // Where the guest's copy of QpiContext lives, so a prank can rewrite the caller it observes. + uint32_t guestContextOffset = 0; }; static inline CallContext* activeCallContext(wasm_exec_env_t execEnv) @@ -256,6 +258,44 @@ static void w_logBytes(wasm_exec_env_t execEnv, uint32_t contractIndex, uint32_t hostServices.logBytes(contractIndex, (unsigned char)type, message, size); } +// CC_PRINT lands on the trace and nowhere else: no log id, no qLogger, no tick log range. Everything +// that touches node state goes on to hostServices.cheat, which is testnet-only and checks the context. +static int64_t w_cheat(wasm_exec_env_t execEnv, uint32_t op, uint64_t a, uint64_t b, uint32_t ptrOffset, uint32_t len) +{ + CallContext* callContext = activeCallContext(execEnv); + void* payload = ptrOffset ? nativeAddress(execEnv, ptrOffset) : nullptr; + + if (op == CHEAT_OP_PRINT) + { + if (callContext && callContext->trace) + { + recordCheat((TraceEntry*)callContext->trace, (uint32_t)(a >> 8), (unsigned char)(a & 0xff), b, payload, len); + } + + return 0; + } + + if (!callContext) + { + return CHEAT_ERR_WRONG_CONTEXT; + } + + if (op == CHEAT_OP_PRANK || op == CHEAT_OP_UNPRANK) + { + void* guestContext = callContext->guestContextOffset ? nativeAddress(execEnv, callContext->guestContextOffset) : nullptr; + const bool prank = op == CHEAT_OP_PRANK; + + if (prank && (!payload || len != 32)) + { + return CHEAT_ERR_UNKNOWN_OP; + } + + return prankCheatCaller(callContext->ctx, guestContext, prank ? (const m256i*)payload : nullptr, (int64_t)a); + } + + return hostServices.cheat(callContext->ctx, op, a, b, payload, len); +} + static uint32_t w_getEntity(wasm_exec_env_t execEnv, uint32_t idOffset, uint32_t entityOffset) { CallContext* callContext = activeCallContext(execEnv); diff --git a/src/extensions/wasm/runtime/qpi_services.h b/src/extensions/wasm/runtime/qpi_services.h index 9838a933..fe962bef 100644 --- a/src/extensions/wasm/runtime/qpi_services.h +++ b/src/extensions/wasm/runtime/qpi_services.h @@ -203,14 +203,24 @@ static long long burn(const void* context, long long amount, unsigned int contra return procedureContext(context)->burn(amount, contractIndex); } +// CC_WARP shifts only what the contract observes; the node still commits the real tick and epoch, so a +// warping contract cannot move consensus. Reset per dispatch, and constant-folded away off testnet. +#if defined(TESTNET) +static unsigned int cheatTickOffset = 0; +static unsigned short cheatEpochOffset = 0; +#else +static constexpr unsigned int cheatTickOffset = 0; +static constexpr unsigned short cheatEpochOffset = 0; +#endif + static unsigned short epoch(const void* context) { - return functionContext(context)->epoch(); + return (unsigned short)(functionContext(context)->epoch() + cheatEpochOffset); } static unsigned int tick(const void* context) { - return functionContext(context)->tick(); + return functionContext(context)->tick() + cheatTickOffset; } static int numberOfTickTransactions(const void* context) @@ -424,6 +434,126 @@ static unsigned char distributeDividends(const void* context, long long amountPe return (unsigned char)procedureContext(context)->distributeDividends(amountPerShare); } +// Mirrors QpiContext's protected layout so a prank can rewrite the caller a contract observes. The +// size assert turns any drift in QpiContext into a compile error rather than a silent misread. +struct CheatContextImage +{ + unsigned int currentContractIndex; + int stackIndex; + m256i currentContractId; + m256i originator; + m256i invocator; + long long invocationReward; + unsigned char entryPoint; +}; +static_assert(sizeof(CheatContextImage) == sizeof(QPI::QpiContext), "CheatContextImage out of sync with QPI::QpiContext"); + +static void clearCheatWarp() +{ +#if defined(TESTNET) + cheatTickOffset = 0; + cheatEpochOffset = 0; +#endif +} + +#if defined(TESTNET) +// Sets a balance outright rather than transferring, which is the whole point of a deal. +static long long dealCheatBalance(const m256i& publicKey, long long amount) +{ + const int index = spectrumIndex(publicKey); + const long long current = index < 0 ? 0 : energy(index); + + if (current > amount) + { + return decreaseEnergy(index, current - amount) ? amount : CHEAT_ERR_WRONG_CONTEXT; + } + + if (current < amount) + { + increaseEnergy(publicKey, amount - current); + } + + return amount; +} + +#endif + +// Rewrites the guest's copy of the context. The host's own QpiContext is untouched, so the node still +// bills and attributes the real caller; only what the contract reads changes. Lives here rather than in +// the vtable because the guest address comes from the adapter, and the vtable signature must mirror the +// guest's exactly. +static long long prankCheatCaller(const void* context, void* guestContext, const m256i* caller, long long invocationReward) +{ +#if defined(TESTNET) + const CheatContextImage* hostImage = (const CheatContextImage*)context; + + if (!hostImage || hostImage->entryPoint == (unsigned char)DispatchKind::UserFunction) + { + return CHEAT_ERR_WRONG_CONTEXT; + } + + if (!guestContext) + { + return CHEAT_ERR_WRONG_CONTEXT; + } + + CheatContextImage* image = (CheatContextImage*)guestContext; + + // Unprank restores what the host handed the guest at dispatch, which is the host's own context. + image->originator = caller ? *caller : hostImage->originator; + image->invocator = caller ? *caller : hostImage->invocator; + image->invocationReward = caller ? invocationReward : hostImage->invocationReward; + return image->invocationReward; +#else + (void)context; + (void)guestContext; + (void)caller; + (void)invocationReward; + return CHEAT_ERR_DISABLED; +#endif +} + +// Development cheatcodes, opcode-dispatched behind one ABI row so a new one costs no import. Every +// state-touching opcode is testnet-only, and refusal is always a negative return, never a trap. +// CHEAT_OP_PRINT and the prank opcodes never reach here: the adapter owns the trace and guest memory. +static long long cheat(const void* context, unsigned int op, unsigned long long a, unsigned long long b, void* ptr, unsigned int len) +{ +#if defined(TESTNET) + const CheatContextImage* image = (const CheatContextImage*)context; + + // Every opcode below mutates something a read-only call must not touch. + if (!image || image->entryPoint == (unsigned char)DispatchKind::UserFunction) + { + return CHEAT_ERR_WRONG_CONTEXT; + } + + switch (op) + { + case CHEAT_OP_DEAL: + return (ptr && len == 32) ? dealCheatBalance(*(const m256i*)ptr, (long long)a) : CHEAT_ERR_UNKNOWN_OP; + + case CHEAT_OP_WARP_TICK: + cheatTickOffset += (unsigned int)a; + return (long long)cheatTickOffset; + + case CHEAT_OP_WARP_EPOCH: + cheatEpochOffset = (unsigned short)(cheatEpochOffset + a); + return (long long)cheatEpochOffset; + + default: + return CHEAT_ERR_UNKNOWN_OP; + } +#else + (void)context; + (void)op; + (void)a; + (void)b; + (void)ptr; + (void)len; + return CHEAT_ERR_DISABLED; +#endif +} + } // namespace Wasm::Runtime #endif // LITE_WASM_SC diff --git a/src/extensions/wasm/runtime/trace.h b/src/extensions/wasm/runtime/trace.h index e27f4ad0..f503ce29 100644 --- a/src/extensions/wasm/runtime/trace.h +++ b/src/extensions/wasm/runtime/trace.h @@ -45,6 +45,16 @@ struct LogTrace std::string hex; }; +// One CC_PRINT argument. size == 0 means the value came through by register instead of by pointer. +struct CheatEntry +{ + unsigned int id = 0; + unsigned char part = 0; + unsigned int size = 0; + unsigned long long value = 0; + std::string hex; +}; + struct TraceEntry { unsigned long long sequence = 0; @@ -67,6 +77,7 @@ struct TraceEntry std::vector stateDiff; std::vector hostCalls; std::vector logs; + std::vector cheats; }; // On by default so a debugger attached after the fact still finds the calls that mattered. This header @@ -219,6 +230,20 @@ static inline void recordLog(TraceEntry* entry, unsigned char type, const void* }); } +// The development print channel. Deliberately separate from logs: it consumes no log id, reaches no +// qLogger, and is stripped from the contract before submission. +static inline void recordCheat(TraceEntry* entry, unsigned int id, unsigned char part, unsigned long long value, const void* bytes, unsigned int size) +{ + if (!entry) + { + return; + } + + entry->cheats.push_back(CheatEntry{ + id, part, size, value, size ? hex(bytes, size) : std::string(), + }); +} + } // namespace Wasm::Runtime #endif // LITE_WASM_SC diff --git a/src/extensions/wasm/sdk/lhost_imports.h b/src/extensions/wasm/sdk/lhost_imports.h index 7f3881b0..1a0f3867 100644 --- a/src/extensions/wasm/sdk/lhost_imports.h +++ b/src/extensions/wasm/sdk/lhost_imports.h @@ -91,6 +91,7 @@ LH_IMPORT(liteCallFunction) int lh_liteCallFunction(unsigned int calleeIdx, unsi LH_IMPORT(liteInvokeProcedure) int lh_liteInvokeProcedure(unsigned int calleeIdx, unsigned int inputType, const void* in, unsigned int inSize, void* out, unsigned int outSize, long long invocationReward); LH_IMPORT(liteSetShareholderProposal) unsigned int lh_liteSetShareholderProposal(unsigned int calleeIdx, const void* proposal1024, long long invocationReward); LH_IMPORT(liteSetShareholderVotes) unsigned int lh_liteSetShareholderVotes(unsigned int calleeIdx, const void* voteData, unsigned int voteSize, long long invocationReward); +LH_IMPORT(cheat) long long lh_cheat(unsigned int op, unsigned long long a, unsigned long long b, void* ptr, unsigned int len); } // extern "C" namespace Wasm::Sdk diff --git a/src/extensions/wasm/shared/abi_metadata.h b/src/extensions/wasm/shared/abi_metadata.h index d47e8089..43e8300f 100644 --- a/src/extensions/wasm/shared/abi_metadata.h +++ b/src/extensions/wasm/shared/abi_metadata.h @@ -1,7 +1,7 @@ #pragma once // Canonical rows shared by WAMR registration and SDK metadata generation. -#define WASM_ABI_VERSION 5u +#define WASM_ABI_VERSION 6u // G/H selects generated or handwritten adapters; Q/I selects QPI-bound or infrastructure calls. @@ -81,4 +81,5 @@ HQ("liteCallFunction", liteCallFunction, w_liteCallFunction, "(iiiiii)i") \ HQ("liteInvokeProcedure", liteInvokeProcedure, w_liteInvokeProcedure, "(iiiiiiI)i") \ HQ("liteSetShareholderProposal", setShareholderProposal, w_liteSetShareholderProposal, "(iiI)i") \ - HQ("liteSetShareholderVotes", setShareholderVotes, w_liteSetShareholderVotes, "(iiiI)i") + HQ("liteSetShareholderVotes", setShareholderVotes, w_liteSetShareholderVotes, "(iiiI)i") \ + HQ("cheat", cheat, w_cheat, "(iIIii)I") diff --git a/src/extensions/wasm/shared/abi_types.h b/src/extensions/wasm/shared/abi_types.h index 14d3bea9..f8144354 100644 --- a/src/extensions/wasm/shared/abi_types.h +++ b/src/extensions/wasm/shared/abi_types.h @@ -125,8 +125,26 @@ struct HostServices unsigned short (*setShareholderProposal)(const void* callerCtx, unsigned int calleeIdx, const void* proposal1024, long long invocationReward); unsigned char (*setShareholderVotes)(const void* callerCtx, unsigned int calleeIdx, const void* voteData, unsigned int voteSize, long long invocationReward); + // Testnet development aid: opcode-dispatched, negative return on refusal. See CHEAT_OP_* below. + long long (*cheat)(const void* ctx, unsigned int op, unsigned long long a, unsigned long long b, void* ptr, unsigned int len); }; +// Opcodes for HostServices::cheat. Reserved numbers stay listed so an older node answers CHEAT_ERR_UNKNOWN_OP +// rather than silently accepting a newer client's call. +#define CHEAT_OP_PRINT 1u +#define CHEAT_OP_DEAL 2u +#define CHEAT_OP_WARP_TICK 3u +#define CHEAT_OP_WARP_EPOCH 4u +#define CHEAT_OP_PRANK 5u +#define CHEAT_OP_UNPRANK 6u +#define CHEAT_OP_WARP_TIME 7u // reserved: needs a calendar inverse the node does not have +#define CHEAT_OP_SNAPSHOT 8u // reserved: no world-snapshot primitive exists in either runtime +#define CHEAT_OP_REVERT 9u // reserved, pairs with CHEAT_OP_SNAPSHOT + +#define CHEAT_ERR_UNKNOWN_OP (-1LL) +#define CHEAT_ERR_DISABLED (-2LL) +#define CHEAT_ERR_WRONG_CONTEXT (-3LL) + #define WASM_MAX_USER_ENTRIES 1024 struct ContractDescriptor From 09795609fa14fbefeb391d6b28a2b5d8410e9863 Mon Sep 17 00:00:00 2001 From: feiyu Date: Tue, 1 Sep 2026 00:38:20 +0700 Subject: [PATCH 08/21] Refuse a negative deal amount The amount arrives as an unsigned word, so a value past the signed range landed here negative and would have decreased energy against index -1. --- src/extensions/wasm/runtime/qpi_services.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/extensions/wasm/runtime/qpi_services.h b/src/extensions/wasm/runtime/qpi_services.h index fe962bef..693ff5ec 100644 --- a/src/extensions/wasm/runtime/qpi_services.h +++ b/src/extensions/wasm/runtime/qpi_services.h @@ -460,6 +460,13 @@ static void clearCheatWarp() // Sets a balance outright rather than transferring, which is the whole point of a deal. static long long dealCheatBalance(const m256i& publicKey, long long amount) { + // The amount arrives as an unsigned word, so a value past the signed range lands here negative. + // A negative balance is meaningless, and letting it through would decrease against index -1. + if (amount < 0) + { + return CHEAT_ERR_UNKNOWN_OP; + } + const int index = spectrumIndex(publicKey); const long long current = index < 0 ? 0 : energy(index); From 2e41017093ae311e5ba2cc10520f3e7389580118 Mon Sep 17 00:00:00 2001 From: cyber-pc <165458555+cyber-pc@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:59:58 +0700 Subject: [PATCH 09/21] Update ant bpp9000 (#989) * Don't shortcut the ant colony bypass the score engine. * AntBpp9000: all IDs' tree will share the same root with epoch start spectrum digest. * AntBpp9000: Update bpp9000 task file for epoch 229. --- data/bpp9000.task | Bin 44744 -> 44749 bytes doc/ant_colony_mining.md | 55 ++++--- src/mining/ant_colony/ant_colony.h | 2 +- src/mining/score_bpp9000.h | 38 +++-- src/mining/score_engine.h | 54 ++++-- src/network_messages/ant_colony_message.h | 2 +- src/public_settings.h | 4 +- src/qubic.cpp | 2 +- src/score.h | 25 +-- test/data/bpp9000.task | Bin 44744 -> 44749 bytes test/data/gt_ant_production.csv | 128 +++++++-------- test/data/gt_production.csv | 192 +++++++++++----------- test/score.cpp | 53 +++--- 13 files changed, 301 insertions(+), 254 deletions(-) diff --git a/data/bpp9000.task b/data/bpp9000.task index 8ffa5e17a95960f758ddb469c1df8bb8f95821e4..38ce23517c59010f350da7a6e0f5a5694b24c346 100644 GIT binary patch delta 937 zcmXw%IZi@R6oxM#&M3|UC=Z-x#R*4PfSp*d0TVZ1`*vVrEVZ*VR@OFFCdMVW1{>-9 zK8!c{c-(v6Ip;tBdGBAz&#z?d?4n*zgb)V7=Rt`dRt-{c{n*S#>HB6W-i(H>KDx_C zcb{*Kn?k;E`TCIg`M#}1@gDC@K@t|gRfc`oh7(wYIT!$IrC}bH;0T6c29CizMHqq( z=!Z^d1M{wdb=+4&h{G&@Tnh2!U04S$!zL_($q!)?cHk7c!5D=q!rm2XGsY^$LK_f) zqCGReLX4?WzvY}sytJz^XoqRAYXcPK-XwishYHf=1NbkdPzzdk1oB{g`&Fgp+n*sC3cA#}x zaMu3!MfZv_sq-^HTTNI2yYb$=cM9_9TovYd0>(i%Eo?2vW~|~ELEP%hVDV*VZZ~#f z;4*lwl|C!royjbuGwzMoDcvA(%Vym(pTdl@0hi}CIv39W{-`Pfilz|198Yq*jKsQxvfKpT$*@04J2{AXd z0^b*MtG>MZ?mPEC=lu8Pvk-kRM04AFyStGf2wK4B;s8I49K<00eSehs*nc>?dpkTh z-df6ZZC>AAKBs=Zt}aer&QES~VZN5Q1PsFlw7~*IVG#PE66T=?nqUYzpbJ*Pw9T*x zBcPTVjulX26ej-Pc?1nW?tGABVgiMyzyj9(=d(N&ceA1g@umqj3PX3^RNeBwqMJw1vJ6PNpC|VCyFasfsgWpO8`|ck1#=!#YScfgh zLLJ?pBaM(He=%5LKWLi?bVFAZ^C;{>5s%flaxg&%ec+mfdo;SHjxipI-_+=Z(h;hw zX5Owt@D3?tQf+diK@du-$62PPjG=>ROl$s;fgDmwC7w`C^9_ zVUQcN+@y+yV4#=JWS*N{*r74n?^*&ZY7P|FB@?@5r#9eR%~JAm=Ig=EUlOa|d hDX4V{T7U`dKu1jKJy`|?y%+vdLhw?hU!P*h$6w%SHXi^0 diff --git a/doc/ant_colony_mining.md b/doc/ant_colony_mining.md index 881d6501..4a996840 100644 --- a/doc/ant_colony_mining.md +++ b/doc/ant_colony_mining.md @@ -45,9 +45,10 @@ Standalone mining searches alone: every attempt starts from scratch. Ant-colony - Every **mining identity** (a computor or candidate public key) owns its **own tree** - the colony is a per-identity forest. A pool's workers extend the tree of the computor they mine for. -- Each tree starts from a **virtual root**: a starting solution derived from that identity's public key - and the epoch's spectrum digest. It is fixed for the epoch, identical every time you derive it, and - is never stored or submitted. +- Every tree starts from the same **virtual root**: one starting solution per epoch, derived from the + epoch's spectrum digest alone - identical for all identities, identical every time you derive it, + and never stored or submitted. All identities search from one shared origin; the trees branching + from it stay per-identity. - To mine, you pick a **parent** (the root, or any node already in your tree), **inherit** it, vary it under your nonce, and score the result. - If the result **strictly beats the parent** and clears the epoch **threshold**, you **submit** it. On @@ -58,7 +59,7 @@ miner starts from there instead of from scratch. The goal of the epoch is the si found anywhere in the forest. ``` - virtual root (per identity, not stored) + virtual root (shared per epoch, not stored) | +----+----+ | | @@ -73,20 +74,20 @@ Concretely, error gates every attachment: it only falls down a branch (a child m and a *start* - a depth-1 child of the root - must clear the threshold. ``` - error = error count, lower is better threshold = 3838 + error = error count, lower is better threshold = 4000 - root ~4044 raw a fresh root sits above 3838; a start must mutate below it + root ~4200 raw the epoch root (same for everyone) sits above 4000; a start must mutate below it | - +-- A 3790 <= threshold ACCEPT (depth-1 start) + +-- A 3900 <= threshold ACCEPT (depth-1 start) | | - | +-- B 3540 < 3790, beats A ACCEPT + | +-- B 3540 < 3900, beats A ACCEPT | | | | | +-- D 3120 < 3540, beats B ACCEPT | | +-- E 3560 not < 3540 REJECT (must beat parent) | | - | +-- C 3700 < 3790, beats A ACCEPT + | +-- C 3700 < 3900, beats A ACCEPT | - +-- X 3900 > threshold REJECT (over threshold) + +-- X 4100 > threshold REJECT (over threshold) Error only falls as you go deeper. The epoch winner is the single lowest-error node found in any identity's forest. @@ -96,7 +97,7 @@ At epoch end the node ranks every identity by its **single best** score and **ha (the number of computors). **Anti-spam deposit.** Each solution a computor publishes on-chain carries a **refundable -1,000,000 QU deposit**, funded by the computor - not the worker. It is returned when the solution is +1000000 QU deposit**, funded by the computor - not the worker. It is returned when the solution is accepted **and** its claimed score matches the node's recompute; otherwise it is kept. So a computor only publishes solutions it has already validated, and an honest, correct one costs nothing. @@ -105,7 +106,7 @@ only publishes solutions it has already validated, and an honest, correct one co ## Part 2 - Miner / pool integration guide **In short.** A miner works one identity's tree. It reads the epoch context, takes a **parent** (the -identity's virtual root, or a node already in the tree), picks a canonical **nonce**, inherits the +epoch's shared virtual root, or a node already in the tree), picks a canonical **nonce**, inherits the parent's network, and **mutates and scores** it - reproducing the node's score exactly. If the result **beats its parent** and **clears the threshold**, it hands the solution to the **computor**, which re-checks it and **publishes it on-chain**; every node then recomputes the score, folds it into @@ -117,8 +118,8 @@ scorer** - the tree, gates, deposit, and queries are the wrapper around it. 1. **Epoch context** - `REQUEST_ANT_EPOCH_CONTEXT` (public). Read the threshold, freshness window, epoch spectrum digest, and child cap for this epoch, and **verify your task file** against the returned `topologyHash` / `dataHash` (section 2.7a) before doing any work. -2. **Get a starting point** - derive your identity's virtual root, or fetch an existing node you want - to extend (`REQUEST_ANT_PARENT_ANN`). +2. **Get a starting point** - derive the epoch's shared virtual root (from the spectrum digest), or + fetch an existing node you want to extend (`REQUEST_ANT_PARENT_ANN`). 3. **Pick a parent** - the root, or any node in your own tree. 4. **Search** - choose a nonce (section 2.2), inherit the parent LUT, run the mutation walk, score (section 2.3). @@ -153,17 +154,20 @@ knobs are not two solutions. ### 2.3 Scoring - bpp9000 (must be bit-exact) Throughout, `publicKey` is the **mining identity you are extending** - the computor you mine for, which -becomes the transaction's `sourcePublicKey`. Derive the root and the mutation seed from **that** key, -not your worker key, or the node's recompute will not match yours. +becomes the transaction's `sourcePublicKey`. The **mutation seed** derives from **that** key, not your +worker key, or the node's recompute will not match yours. The **root** derives from no key at all - +see below. -**Root.** `deriveRootANN(publicKey, epochPool)`: `K12(publicKey)` seeds a per-neuron LUT from the -epoch's random pool (the pool comes from the epoch-start spectrum digest). No mutation walk. Never -stored. The same every time for the epoch. +**Root.** `deriveRootANN(spectrumDigest, epochPool)`: `K12(spectrumDigest)` - the epoch-start +spectrum digest from the epoch context - seeds a per-neuron LUT from the epoch's random pool (the +pool itself also comes from that digest). No mutation walk. Never stored. **One root per epoch, +identical for every identity**; per-identity variation enters only through the mutation seeds. **Child.** `computeScoreFromParent(parentLUT, publicKey, nonce, anchorTickDigest)`: 1. Inherit `parentLUT`. -2. `mutationSeed = K12(publicKey || nonce[3..31] || anchorTickDigest)` (`nonce[0..2]` zeroed). +2. `mutationSeed = K12(publicKey || nonce[3..31] || anchorTickDigest)` (`nonce[0..2]` zeroed) - + still keyed by the mining identity, so different identities walk differently from the shared root. 3. Walk `numberOfMutations = 100` steps. Each step rewrites `L` LUT entries. For the first `K` steps accept a worse-or-equal result (**explore**); after that accept only better-or-equal (**exploit**); one-step rollback on reject. Keep and return the **best** score seen. The best is seeded with the @@ -201,14 +205,15 @@ recorded but the **deposit is kept** and the miner is **not ranked**. **Starting a tree.** The root's record score is the worst possible value, so a first (depth-1) child passes the "beats parent" check trivially - the **threshold is the only score gate** for starting a -tree. A random root scores far above the threshold, so a start still requires real mutation. +tree. The shared epoch root scores far above the threshold, so a start still requires real mutation - +and every identity starts from the same score, so ranking differences reflect search effort only. **`ValidNotStored`.** Accepted, refunded, and ranked exactly like `Valid`, but the per-epoch store was full so the node was not persisted for others to extend. Ranking and refund are unaffected. ### 2.5 The deposit -Every on-chain `AntColonyMiningSolutionTransaction` carries a **1,000,000 QU** deposit +Every on-chain `AntColonyMiningSolutionTransaction` carries a **1000000 QU** deposit (`SOLUTION_SECURITY_DEPOSIT`), funded by the **computor** that publishes it - not the miner (see 2.6). It is refunded **iff** the solution is accepted (`Valid` / `ValidNotStored`) **and** the claimed score equals the node's recompute; otherwise it is kept. So a computor risks its own deposit and therefore @@ -254,7 +259,7 @@ AntColonyMiningSolutionTransaction : Transaction { // 80-byte header + 48-byte // --- Transaction header --- m256i sourcePublicKey; // the COMPUTOR (tree owner); signs the tx and funds the deposit m256i destinationPublicKey; // zero (NULL_ID) - long long amount; // SOLUTION_SECURITY_DEPOSIT = 1,000,000 QU + long long amount; // SOLUTION_SECURITY_DEPOSIT = 1000000 QU unsigned int tick; // publish tick unsigned short inputType; // ANT_COLONY_MINING_SOLUTION_INPUT_TYPE = 12 unsigned short inputSize; // 48 @@ -301,7 +306,7 @@ signature = sign(operatorSubseed, operatorPublicKey, digest) // 64 bytes, appe Request: empty. Response `RespondAntEpochContext` (120 bytes, packed): ``` -m256i spectrumDigest; // epoch-start spectrum digest (seeds every root) +m256i spectrumDigest; // epoch-start spectrum digest; IS the root seed (and seeds the pool) m256i topologyHash; // canonical task topology-block hash (BPP9000_TOPOLOGY_HASH) m256i dataHash; // canonical task data-block hash (BPP9000_DATA_HASH) unsigned int threshold; // per-epoch accept bound @@ -364,7 +369,7 @@ byte, the exact form the scorer consumes - no unpacking needed): unsigned int parentRefTick; unsigned int parentRefSolutionIndexInTick; unsigned int annSizeBytes; // ANN LUT size when status is OK, else 0 -unsigned char status; // 0 = OK, 1 = NOT_FOUND, 2 = IS_ROOT (derive your own root instead) +unsigned char status; // 0 = OK, 1 = NOT_FOUND, 2 = IS_ROOT (derive the epoch root instead) unsigned char padding[3]; ``` diff --git a/src/mining/ant_colony/ant_colony.h b/src/mining/ant_colony/ant_colony.h index b900f6a6..db935e47 100644 --- a/src/mining/ant_colony/ant_colony.h +++ b/src/mining/ant_colony/ant_colony.h @@ -444,7 +444,7 @@ class AntColony // Constraint specific functions // Resolves a parent for scoring. outParentRec is null for ROOT_REF, the caller derives the - // per-identity root from the submitter's pubkey instead. + // shared epoch root from the root seed instead. ValidityResult tryGetParent(const SolutionRef& parentRef, const AntSolutionRecord** outParentRec) const; diff --git a/src/mining/score_bpp9000.h b/src/mining/score_bpp9000.h index 40468c8c..039a98e6 100644 --- a/src/mining/score_bpp9000.h +++ b/src/mining/score_bpp9000.h @@ -11,16 +11,6 @@ namespace score_engine // Largest L (mutations per step) the scorer clamps nonce[1] to static constexpr unsigned int MAX_LUT_ENTRIES_PER_STEP = 10; -// A bpp9000 nonce is canonical iff its score-irrelevant knob bytes are canonical: -// nonce[0] = algo (enforced by routing), nonce[1] = L in [1, MAX_LUT_ENTRIES_PER_STEP], nonce[2] = K = 0 -static bool isCanonicalBpp9000Nonce(const unsigned char* nonce) -{ - return (getAlgoType(nonce) == AlgoType::Bpp9000) - && (nonce[1] >= 1) - && (nonce[1] <= MAX_LUT_ENTRIES_PER_STEP) - && (nonce[2] == 0); -} - template struct ScoreBpp9000 { @@ -45,6 +35,17 @@ struct ScoreBpp9000 static_assert(lutSize <= lutStride, "LUT rows must fit the padded stride"); + // A nonce is canonical if its score-irrelevant knob bytes are canonical: + // nonce[0] = algo (enforced by routing), nonce[1] = L in [1, MAX_LUT_ENTRIES_PER_STEP], + // nonce[2] = K. The standalone walk pins K to 0, so only 0 is canonical there. + static bool isCanonicalStandaloneNonce(const unsigned char* nonce) + { + return (getAlgoType(nonce) == AlgoType::Bpp9000) + && (nonce[1] >= 1) + && (nonce[1] <= MAX_LUT_ENTRIES_PER_STEP) + && (nonce[2] == 0); + } + // K is a real degree of freedom here: the walk restores K = nonce[2] as its explore-step count static bool isCanonicalAntNonce(const unsigned char* nonce) { @@ -1073,8 +1074,9 @@ struct ScoreBpp9000 compact(bestANN, out); } - // Seed the ANN: root LUT from the pubkey alone (each computor's fixed root); mutation seeds from - // pubkey+nonce (nonce[0..2] are the algo/L/K knobs, excluded from the RNG). Returns the start score. + // Standalone path only: root LUT from the pubkey alone; the ant path derives its shared epoch + // root via deriveRootANN(rootSeed) instead. Mutation seeds from pubkey+nonce (nonce[0..2] are + // the algo/L/K knobs, excluded from the RNG). Returns the start score. unsigned int initializeANN( const unsigned char* publicKey, const unsigned char* nonce, @@ -1168,14 +1170,16 @@ struct ScoreBpp9000 return computeScoreFromCurrent(L, K, cur); } - // Ant colony: the network every one of an identity's lineages starts from. Written to a buffer the - // caller owns, so two roots can be derived on one engine without the first silently becoming the - // second - a child scored against the wrong root would differ only in resourceTestingDigest. + // Ant colony: the shared per-epoch network every identity's tree starts from. rootSeed is the + // epoch-start spectrum digest, so all identities derive the identical root; only the mutation + // walks stay per-identity. Written to a buffer the caller owns, so two roots can be derived on + // one engine without the first silently becoming the second - a child scored against the wrong + // root would differ only in resourceTestingDigest. // Uses currentANN as its working buffer, so it destroys whatever the engine was holding. Callers // derive a root and then score from it, which overwrites currentANN anyway. - void deriveRootANN(const unsigned char* publicKey, const unsigned char* pRandom2Pool, ANN& out) + void deriveRootANN(const unsigned char* rootSeed, const unsigned char* pRandom2Pool, ANN& out) { - deriveRootLut(publicKey, pRandom2Pool); + deriveRootLut(rootSeed, pRandom2Pool); applyRootLut(currentANN); compact(currentANN, out); } diff --git a/src/mining/score_engine.h b/src/mining/score_engine.h index a95cb048..a512d5ad 100644 --- a/src/mining/score_engine.h +++ b/src/mining/score_engine.h @@ -12,6 +12,9 @@ struct ScoreEngine ScoreBpp9000 _bpp9000Score; unsigned char lastNonceByte0; + // The inheritable per-neuron LUT the ant colony branches on. + using AntAnn = typename ScoreBpp9000::ANN; + void initMemory() { setMem(&_bpp9000Score, sizeof(ScoreBpp9000), 0); @@ -19,12 +22,6 @@ struct ScoreEngine _bpp9000Score.initMemory(); } - // Unused function - void initMiningData(const unsigned char* randomPool) - { - - } - // Load the task blocks into the active bpp9000 leaf; returns false on invalid topology/data. bool loadTask(const unsigned char* topoBlock, const unsigned char* dataBlock) { @@ -39,8 +36,6 @@ struct ScoreEngine unsigned int computeBpp9000Score(const unsigned char* publicKey, const unsigned char* nonce, const unsigned char* randomPool) { - // The score IS the error count - smaller is better. A timeout maps to the worst in-range value - // rather than INVALID_SCORE_VALUE, which the score cache cannot store (it reads back as a miss). const unsigned int failures = _bpp9000Score.computeScore(publicKey, nonce, randomPool); return (failures == ScoreBpp9000::INFINITE_ERROR) ? (unsigned int)ScoreBpp9000::numberOfWindows @@ -61,9 +56,19 @@ struct ScoreEngine } } - // Each engine owns its canonical ant-nonce rule; this switch is the algorithm seam, so ingress - // code stays algorithm-agnostic. Neuraxon is reserved and not ant-minable, so no nonce in its - // slot is canonical. + // Each engine owns its canonical standalone-nonce rule + static bool isCanonicalStandaloneNonce(const unsigned char* nonce) + { + switch (getAlgoType(nonce)) + { + case AlgoType::Bpp9000: + return ScoreBpp9000::isCanonicalStandaloneNonce(nonce); + default: + return false; + } + } + + // Each engine owns its canonical ant-nonce rule static bool isCanonicalAntNonce(const unsigned char* nonce) { switch (getAlgoType(nonce)) @@ -75,6 +80,33 @@ struct ScoreEngine } } + // Ant colony: the shared per-epoch network every identity's tree starts from; rootSeed is the + // epoch-start spectrum digest + void deriveAntRootANN(const unsigned char* rootSeed, const unsigned char* randomPool, AntAnn& out) + { + _bpp9000Score.deriveRootANN(rootSeed, randomPool, out); + } + + // Ant colony: score a child by inheriting the parent's network and walking it with the child's + // own seeds. Returns INVALID_SCORE_VALUE for a non-canonical nonce or an unsupported algorithm. + unsigned int computeAntScoreFromParent(const AntAnn& parent, const unsigned char* publicKey, + const unsigned char* nonce, const unsigned char* anchorDigest, const unsigned char* randomPool) + { + switch (getAlgoType(nonce)) + { + case AlgoType::Bpp9000: + return _bpp9000Score.computeScoreFromParent(parent, publicKey, nonce, anchorDigest, randomPool); + default: + return INVALID_SCORE_VALUE; + } + } + + // Ant colony: the network that produced the score the walk returned. + void getAntBestANN(AntAnn& out) + { + _bpp9000Score.getBestANN(out); + } + // returns last computed output neurons of the active bpp9000 slot m256i getLastOutput() { diff --git a/src/network_messages/ant_colony_message.h b/src/network_messages/ant_colony_message.h index 98b123eb..10c31ed2 100644 --- a/src/network_messages/ant_colony_message.h +++ b/src/network_messages/ant_colony_message.h @@ -95,7 +95,7 @@ static_assert(sizeof(AntIdentityTreeResponse) // RespondAntParentAnnHeader.status values. constexpr unsigned char ANT_PARENT_ANN_STATUS_OK = 0; // ANN bytes follow the header constexpr unsigned char ANT_PARENT_ANN_STATUS_NOT_FOUND = 1; // parentRef has no record -constexpr unsigned char ANT_PARENT_ANN_STATUS_IS_ROOT = 2; // ROOT_REF; no ANN payload - miner derives its own per-identity root +constexpr unsigned char ANT_PARENT_ANN_STATUS_IS_ROOT = 2; // ROOT_REF; no ANN payload - miner derives the shared epoch root // ONE tree node's stored network, named by parentRef - the ANN state a miner mutates to extend // that node. The tree itself is listed by the identity-tree query; this fetches the material for a diff --git a/src/public_settings.h b/src/public_settings.h index d4920889..c1e3c024 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -118,8 +118,8 @@ static constexpr unsigned int NEURAXON_SOLUTION_THRESHOLD_DEFAULT = 1; // and hash-verified at node init static unsigned short SCORE_BPP9000_TASK_FILE_NAME[] = L"bpp9000.task"; static constexpr unsigned char BPP9000_TOPOLOGY_HASH[32] = - { 0x13, 0xe9, 0x9d, 0x5b, 0x2f, 0xca, 0x56, 0xaa, 0x78, 0x9c, 0xb9, 0x59, 0x57, 0x5f, 0x48, 0x39, - 0x2f, 0x1a, 0x44, 0x90, 0x9a, 0x8e, 0xaf, 0x27, 0xf2, 0xde, 0x8f, 0x8d, 0x74, 0xb0, 0x7a, 0x6b }; + { 0x76, 0xa3, 0xf5, 0x10, 0x20, 0x05, 0x9b, 0xf5, 0x22, 0x7f, 0x30, 0x20, 0x13, 0x69, 0xcb, 0x0a, + 0x32, 0x3b, 0x93, 0xcd, 0xc2, 0x59, 0x8e, 0x1f, 0x1c, 0x59, 0x66, 0xbd, 0x9c, 0x0d, 0xf4, 0xef }; static constexpr unsigned char BPP9000_DATA_HASH[32] = { 0x97, 0x9c, 0xdc, 0x22, 0x47, 0xd2, 0xca, 0x4e, 0xd3, 0xd6, 0x14, 0xbf, 0x27, 0x89, 0x63, 0x84, 0xcb, 0x1c, 0x9c, 0x3d, 0x80, 0x4a, 0xf6, 0xed, 0xe6, 0xb5, 0x9f, 0xc5, 0x2c, 0x0e, 0x3d, 0xfa }; diff --git a/src/qubic.cpp b/src/qubic.cpp index b5bd493f..621fae50 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -3440,7 +3440,7 @@ static void processTickTransactionAntColonySolution( } else { - // A null parent record means root, the scorer derives the submitter's own root, since roots + // A null parent record means root, the scorer derives the shared epoch root, since roots // are never stored and so cannot be handed in. const AntColonyBpp9000T::Ann* parentAnn = nullptr; if (parentRec != nullptr) diff --git a/src/score.h b/src/score.h index 31f0772c..5798e8a4 100644 --- a/src/score.h +++ b/src/score.h @@ -203,7 +203,8 @@ struct ScoreFunction // Ant colony main score function // score a child by inheriting its parent's network and walking it with the child's own seeds. - // parentAnn == nullptr means the parent is the submitter's root, which is derived here from the pubkey + // parentAnn == nullptr means the parent is the epoch root, which is derived here from the + // epoch-start spectrum digest (currentRandomSeed) and is identical for every identity // Returns INVALID_SCORE_VALUE for a non-canonical nonce, in which case outChildAnn is not written // bestANN would still hold the previous call's network, and committing that would put one node's // stale bytes into childAnnHash. @@ -217,25 +218,25 @@ struct ScoreFunction { const int solutionBufIdx = (int)(processor_Number % solutionBufferCount); LockGuard guard(solutionEngineLock[solutionBufIdx]); - score_engine::ScoreBpp9000T& engine = _computeBuffer[solutionBufIdx]._bpp9000Score; + score_engine::ScoreEngineT& engine = _computeBuffer[solutionBufIdx]; // Derived into this slot's scratch rather than the engine's own buffer: deriveRootANN() uses // currentANN as working space const score_engine::ScoreBpp9000T::ANN* parent = parentAnn; - // Depth 1, the start node of every public key + // Depth 1, the shared epoch root every identity starts from if (parent == nullptr) { - engine.deriveRootANN(publicKey.m256i_u8, poolVec, _antRootScratch[solutionBufIdx]); + engine.deriveAntRootANN(currentRandomSeed.m256i_u8, poolVec, _antRootScratch[solutionBufIdx]); parent = &_antRootScratch[solutionBufIdx]; } - const unsigned int childScore = engine.computeScoreFromParent( + const unsigned int childScore = engine.computeAntScoreFromParent( *parent, publicKey.m256i_u8, nonce.m256i_u8, anchorDigest.m256i_u8, poolVec); if (childScore == score_engine::INVALID_SCORE_VALUE) { return childScore; } - engine.getBestANN(outChildAnn); + engine.getAntBestANN(outChildAnn); return childScore; } // main score function @@ -243,17 +244,9 @@ struct ScoreFunction { PROFILE_SCOPE(); - switch (score_engine::getAlgoType(nonce.m256i_u8)) + if (!score_engine::ScoreEngineT::isCanonicalStandaloneNonce(nonce.m256i_u8)) { - case score_engine::AlgoType::Bpp9000: - if (!score_engine::isCanonicalBpp9000Nonce(nonce.m256i_u8)) - { - return score_engine::INVALID_SCORE_VALUE; - } - break; - default: - // Unsupported algo - return score_engine::INVALID_SCORE_VALUE; + return score_engine::INVALID_SCORE_VALUE; } if (isZero(miningSeed) || miningSeed != currentRandomSeed) diff --git a/test/data/bpp9000.task b/test/data/bpp9000.task index 8ffa5e17a95960f758ddb469c1df8bb8f95821e4..38ce23517c59010f350da7a6e0f5a5694b24c346 100644 GIT binary patch delta 937 zcmXw%IZi@R6oxM#&M3|UC=Z-x#R*4PfSp*d0TVZ1`*vVrEVZ*VR@OFFCdMVW1{>-9 zK8!c{c-(v6Ip;tBdGBAz&#z?d?4n*zgb)V7=Rt`dRt-{c{n*S#>HB6W-i(H>KDx_C zcb{*Kn?k;E`TCIg`M#}1@gDC@K@t|gRfc`oh7(wYIT!$IrC}bH;0T6c29CizMHqq( z=!Z^d1M{wdb=+4&h{G&@Tnh2!U04S$!zL_($q!)?cHk7c!5D=q!rm2XGsY^$LK_f) zqCGReLX4?WzvY}sytJz^XoqRAYXcPK-XwishYHf=1NbkdPzzdk1oB{g`&Fgp+n*sC3cA#}x zaMu3!MfZv_sq-^HTTNI2yYb$=cM9_9TovYd0>(i%Eo?2vW~|~ELEP%hVDV*VZZ~#f z;4*lwl|C!royjbuGwzMoDcvA(%Vym(pTdl@0hi}CIv39W{-`Pfilz|198Yq*jKsQxvfKpT$*@04J2{AXd z0^b*MtG>MZ?mPEC=lu8Pvk-kRM04AFyStGf2wK4B;s8I49K<00eSehs*nc>?dpkTh z-df6ZZC>AAKBs=Zt}aer&QES~VZN5Q1PsFlw7~*IVG#PE66T=?nqUYzpbJ*Pw9T*x zBcPTVjulX26ej-Pc?1nW?tGABVgiMyzyj9(=d(N&ceA1g@umqj3PX3^RNeBwqMJw1vJ6PNpC|VCyFasfsgWpO8`|ck1#=!#YScfgh zLLJ?pBaM(He=%5LKWLi?bVFAZ^C;{>5s%flaxg&%ec+mfdo;SHjxipI-_+=Z(h;hw zX5Owt@D3?tQf+diK@du-$62PPjG=>ROl$s;fgDmwC7w`C^9_ zVUQcN+@y+yV4#=JWS*N{*r74n?^*&ZY7P|FB@?@5r#9eR%~JAm=Ig=EUlOa|d hDX4V{T7U`dKu1jKJy`|?y%+vdLhw?hU!P*h$6w%SHXi^0 diff --git a/test/data/gt_ant_production.csv b/test/data/gt_ant_production.csv index 664c7574..48aa11ed 100644 --- a/test/data/gt_ant_production.csv +++ b/test/data/gt_ant_production.csv @@ -1,65 +1,65 @@ chain, depth, pubkey, nonce, anchor, seed, score -2, 0, 886ef7f14d42287f8a74aaa92a409eaf901e25b6f9cfbedb5f72e1297bcad551, 01084ed07b9200fe2bb401df72b52ba819fd5c09e436f5f185594f8b8afcbc66, 54ba8fded70f55d660977d169d6a2ab6d7711b11f2b49596fbab1d5bf968a961, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4576 -5, 0, 2df29076f8969739636dff083eb1f54bfe7e5d8fcac5b2946d0871d40ba495aa, 0106216c6da947657f74a629a59d5b81da7a4a6ecce8a3abb4b23079b5f1c56e, 21c6ec71261f8027ee7fe4629982e12c50facf8d0acc1f8f297264db6bc2dc2c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 6400 -11, 0, 83b379182516db53587b3c553468e1af0ca4c21f13b95150024e68ebb2bb9550, 01085ebfe74c8324c42214dde9c3c71f691e327cd8a764e4c396d7431deaae03, 12545454084db67b036475332b9a7ebd7c7ed73b13e64bc17d87ec2e758dcb22, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5308 -1, 0, 17620b9920ef596dfa46753b355247b1641eaeda1246fa7bc43884630cf61bf2, 010523bbe90fa21b263946830cc53e8744e270cf6e944b1cf255e85ac74a7887, eae546338da0b54a3b4e10f859b6293f1d285a0e51e4881f10d1379ff511f819, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 6295 -0, 0, 4657de22da43ad6402c8cfdd45ba54542213486481988bdd28c82182ec2714af, 01023378b9bf89fae25fdcd165c8739da9dbba04ff92982345a623031d23b0fb, 91a1d4fc06689515127b01f116fd64c4c2ca4c02a4692675c1b0dd4fc1bf3589, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4800 -8, 0, e0b3b0f9b66fe475eaf17c6be38a2759ff254debd5485c4d4a27bc45486fa0ee, 010733ccce2e806a4aea54c0dcc386ebfd4efc6f3027f63c1de301686508f403, a2050b44a20027fb98984b156632263a8bccefd34e4c1a06d516ac807ea19433, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4414 -6, 0, dc40c51265eff259556066d427dbeebb4109a7e79690d61d6349ef650b29dcb9, 010a33855fd20d0f1c910f0f7f7bc4eef7dfde506c7184d12bb6e50dc095b9de, 0509c75356fc65087c969b8eb8602a28c903a34d8f0648394e29dbeaa9cb4892, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4206 -10, 0, eda9ead70a463d6bf5858faf2a8caa3c3d20b3c3e0e994167e685200bc750276, 01080bf6551a06f4826c9dfb047814381042f281b322ab9452623183daab9dd3, a0651819278fad70d8d2e24db8fa595f305cb879b400fb0c270ecad1361cc25c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4352 -4, 0, e5e05648b1c70c4deea4c7d7aefccd49b24defadceb45dd033caf0f16407808b, 0105337415ffe5aae3f1e4816712ba7ca6e4df3db6b10276868d4e2861cb69e8, a18598ffcfd37339a922658ea450e94f2e4cd083c4e64d5013d002c4fcb17743, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4379 -6, 1, dc40c51265eff259556066d427dbeebb4109a7e79690d61d6349ef650b29dcb9, 010a54a6afb54d321417a986b3e8ce05e51823ea0aceef2681392fcda42010b0, bb849545500e6716bdbbf6fbbdcf6bfe1fd2b4cc20c1024cb419be77dae27df0, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4206 -5, 1, 2df29076f8969739636dff083eb1f54bfe7e5d8fcac5b2946d0871d40ba495aa, 010354eaf1f371ea113434301d5e15b04d24b92fc11dd20f5e03fb6e6f497d30, e9bf8e92a20da221725f0897cf111f326c7754ead360f7aba074459ad99ec7f9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5998 -3, 0, 072fef44ac2f7667c366f067b374016bdd39581dbe8acb0f8aadf98502d13646, 010329371769a20be8033c7868d60327ad8b345b190f69f6a6b54f55c7acde8c, a3e0ad1361d551b2ce13025624602a4ebbcb0bc37c93348a88d23d394c155bd9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4569 -2, 1, 886ef7f14d42287f8a74aaa92a409eaf901e25b6f9cfbedb5f72e1297bcad551, 01081abdbc10a2b70d301fe0bb0afa298f90a5d9bdd0777d24949228abc63106, 02724836ecf8a9bd54e8f78867ea5b4e5c13dc6c41566094a6b41fa36e219f09, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4576 -8, 1, e0b3b0f9b66fe475eaf17c6be38a2759ff254debd5485c4d4a27bc45486fa0ee, 010229cda723c097587f19e30ee090700b7da9bfc9a24e2fb73d83d55b4ba107, c55bea5399c1ca80efe9b9b6c51991ce99b6cc34f804984957cc7a110a1e03cd, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4373 -0, 1, 4657de22da43ad6402c8cfdd45ba54542213486481988bdd28c82182ec2714af, 010615c66cb4c65a74382fce9000de43248a4425ced3bc79929139502e97c705, 4a564c2984b914fda7e68a1c80fd1bfdf6a75ac2443f348e98645d42fbaa052a, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4450 -10, 1, eda9ead70a463d6bf5858faf2a8caa3c3d20b3c3e0e994167e685200bc750276, 010a5e540d6e35170556e51d0a25400b8e7dce3a21872d56e397633411fc7656, eb23ade51e8662688b5e069f59e64c9049c7896058ee4460bab704180f8be6e6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4352 -1, 1, 17620b9920ef596dfa46753b355247b1641eaeda1246fa7bc43884630cf61bf2, 01050a3c2127a028962e38ab80d9eb6848790106a3809637f8b1eb849c261a1b, 8dc4c241e049f0be5341d03b0d1023ae5340cd8cc506dd6f03a998743e2fed75, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4663 -2, 2, 886ef7f14d42287f8a74aaa92a409eaf901e25b6f9cfbedb5f72e1297bcad551, 010246c5bb2fa79a2bc5f2ed1fbb2877ba2de90011ee439383347c684cfb80a6, ef9bab5a83f63baadbd79d9e3680a36eeb65d8c9c1c52c6dce0e47f807ccb465, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4394 -11, 1, 83b379182516db53587b3c553468e1af0ca4c21f13b95150024e68ebb2bb9550, 01025ded1ea31c0763dd89595fce37c703c886203ef104ccea9979bf6041b511, 967e0547550da817764ba0a7b51e33556c2435e9a910e4b16d9300bd785a06e1, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5308 -4, 1, e5e05648b1c70c4deea4c7d7aefccd49b24defadceb45dd033caf0f16407808b, 0106282d2480193df99350d4ef0c3b31b9a4314b38cc3d0e86c3c9564b9fc77b, 9ba91514fec4c80a40201e7fbc9bf7d08ee446b8f3efaff16e32ae54c5afdc92, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4310 -6, 2, dc40c51265eff259556066d427dbeebb4109a7e79690d61d6349ef650b29dcb9, 01052e993e876de4beeed6b7e5f674acf08d15f44a41dc59e080b50b5e9ee9ee, a570a04a90af0b23bf9ea5b4be54314a853f517a060c39df1d983e9665f4c45b, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4206 -3, 1, 072fef44ac2f7667c366f067b374016bdd39581dbe8acb0f8aadf98502d13646, 0109595f50ab0a4d9c0c2209dc2e020058a6a3fcaa4ab687f5338d46a4259031, b8a420a950c50a97f8876a589cc1a4f2822eed80efc2e7ef6bd44b2c780061c6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4569 -5, 2, 2df29076f8969739636dff083eb1f54bfe7e5d8fcac5b2946d0871d40ba495aa, 01030cc6084cbef2f92c8fc481db9b3b36ec91723770bb5c6aaea1dfb2104376, b3310ce38053f82c73d26eed58ad00897a8d0de995d60c4eeb8d4755afc518c9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4836 -10, 2, eda9ead70a463d6bf5858faf2a8caa3c3d20b3c3e0e994167e685200bc750276, 01040809aeb747d430f914d0ceb1970d3ff7e50b44ac1db1de8ac6a910795423, 5df3e71634f680908f5954ce538b65f470d65779dfcae051f1d42b43f1527abe, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4241 -10, 3, eda9ead70a463d6bf5858faf2a8caa3c3d20b3c3e0e994167e685200bc750276, 01075358c48d3c17682f8c69578d2db1710168ebad922b591b764b0937353be2, 190832364108015065a051e38badc3535be4faff42a319132ebfecea4bf03416, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4241 -8, 2, e0b3b0f9b66fe475eaf17c6be38a2759ff254debd5485c4d4a27bc45486fa0ee, 010307ae588e0d66befcfb11099713189200bfeab97cb37d4f18bfa0d877cd2a, 2032dcba5a6c53d1cb68ad800ae8c7b455aef9aa90e1c14cb5591bbe2c8e1927, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4307 -6, 3, dc40c51265eff259556066d427dbeebb4109a7e79690d61d6349ef650b29dcb9, 01014f1dc6bcc0311403fe50177fe2b69d702b1109db30aab04be2b6cd964375, 7f2fbd2dca697fee9169049bd06a5cadb6932db37cc8190b7f781e2859a13ddf, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4206 -0, 2, 4657de22da43ad6402c8cfdd45ba54542213486481988bdd28c82182ec2714af, 01011280b730b2317044f434b8e56ca881f7efeaf8dd4b29b12e877ea43fb7a1, d260aab327663b1197f602584a74c80dfdc3915d487440e21d59b6d795ba284c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4450 -2, 3, 886ef7f14d42287f8a74aaa92a409eaf901e25b6f9cfbedb5f72e1297bcad551, 0107159b5267736895605a21efe7bfa20f676707dd2bfdccabc6d67ed211d511, 32fa4cc87d7f7ba35dfcc9fad0256dd64bd1c3a340e1fb1ce92a2e3102641be8, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4291 -11, 2, 83b379182516db53587b3c553468e1af0ca4c21f13b95150024e68ebb2bb9550, 01040f59f2e3d85b897c1997ae4cd6a1aa20f0397795106ec35742efa2f60207, 70efb4b8a77579ec350179a71f89e517c378ec87896a526d5bca985e45e6a2fc, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4581 -1, 2, 17620b9920ef596dfa46753b355247b1641eaeda1246fa7bc43884630cf61bf2, 0103333ea8e0bc20c0f2cfb7cc93e9d3ae302f4b5e933e605ca8d5f2b101f7c8, 39cb97cbe391fff147ecffe6bc7b6cfcf920726974c9ad5394b54e7a86d3e752, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4663 -14, 0, 8c2cce72fa295e618dada27cd9dce2a8f24a4596d676edf11e096bcf6f5c078f, 0103553c86f3dcc0e0a8c63a9004bc5719b09b04348d77a9de594f5b8bdffaae, db7e8851e5e2b45c072bf7cd15040a9bc313b6d4e8e568f7a50a62810a181842, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5808 -5, 3, 2df29076f8969739636dff083eb1f54bfe7e5d8fcac5b2946d0871d40ba495aa, 01022aa2e9c6bf0d46e240165a772be18804f052b8f76111e0c22af538b332e6, ed0147d60ab884ab994eca1b25f319349437e4106b21068d0f8ae9c357102e14, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4779 -4, 2, e5e05648b1c70c4deea4c7d7aefccd49b24defadceb45dd033caf0f16407808b, 01080ccfc0cce11c4229f4dd023630330c17f3f495c3db73a30ed50107882a5e, eb7729568d616b19004a42dab7f8b237defd6eb3a0a5448037ecf4ec56044bd7, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4137 -3, 2, 072fef44ac2f7667c366f067b374016bdd39581dbe8acb0f8aadf98502d13646, 01013064dacd09cf7f1898ac0b669d25f4c0db3c2bcea9b3c9cd51f3fc8a7414, 5a107488a0caf086ae94acfefca9b36f6ae84691b11a001ef3999a673d7b160b, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4394 -0, 3, 4657de22da43ad6402c8cfdd45ba54542213486481988bdd28c82182ec2714af, 0109151f9d11847474e333460ea73646a31663c84162313db3cea328a74efa82, c10184404849ef460dd734d49d22cad1b25aafa77bbd3f22073dc88885f644ec, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4450 -4, 3, e5e05648b1c70c4deea4c7d7aefccd49b24defadceb45dd033caf0f16407808b, 01044b182869be4ba5670092661abac68606a2f4b06130d101882d236e482358, b7fc5ca1cb869e33b3725a1f3a4c96e8545200d0f263d099351016b6baf46039, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4137 -14, 1, 8c2cce72fa295e618dada27cd9dce2a8f24a4596d676edf11e096bcf6f5c078f, 01076440117d3ff4d99c831ad310105cb1217c9bfe4bbd7a8d98df63fde9dac1, c01ee3f86edc2dbb38b5fcabf8dca2e5f63e80cfc7161b897783b71fb24eb07b, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5808 -8, 3, e0b3b0f9b66fe475eaf17c6be38a2759ff254debd5485c4d4a27bc45486fa0ee, 01092583572dbd57774dc64402af3254c4e9cfc97a76b686d690e728f4946882, c1003cacbb633a682fa1587cc0a59535501e3d8511b896df7431c320da790724, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4294 -11, 3, 83b379182516db53587b3c553468e1af0ca4c21f13b95150024e68ebb2bb9550, 010522c21f13bb19c26a46e5798d2ce597bfd035261c4a3e9e2d38963910dba2, e692e800bb6e620f5108e70950e6f65c46997a3dd8aef402a30d44870ba94440, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4344 -12, 0, 2814b082c6683e9490338df3e42c8d89ca9e2f889735013dcbb7ddbdf7d846c4, 010618c41d7d80fdeb077f54165537de721e39ee32e5f8d11dd16e5edff70d91, 9c65303cf21df1f289cd5d7e1d431ddd79016d87c6e8ec57b2c23c15c69c119f, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4862 -1, 3, 17620b9920ef596dfa46753b355247b1641eaeda1246fa7bc43884630cf61bf2, 0108598ac27cff46dd5635d43a3ba7b32bf7c0f844c95418fb359f4bb8b14a48, cdb0470fe9f772124a6d158869b4bed2451b4445293ffa2cc56f96917b108ebc, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4663 -12, 1, 2814b082c6683e9490338df3e42c8d89ca9e2f889735013dcbb7ddbdf7d846c4, 010a33039aba1207b1d63d559fbe40220eaa30b56fd25e5d0767eed2538ed35c, 27ee5997236575efdb19c9845b330da3e969ab8b69af32b6cbace636afbca8cc, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4862 -13, 0, e36507d27632e2d6cc70480f6322a4469d20773ff0d047f6f70285e9d44beb5b, 010a602d4d374fe77a551445a50be97aa8ff00a8e6d1bc87eb6a05a7ca9d5bac, 5e65aac82ee69c59e00bd8bc6b688b45c6a30fc14a97c50f631f6255eaaf7327, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4916 -14, 2, 8c2cce72fa295e618dada27cd9dce2a8f24a4596d676edf11e096bcf6f5c078f, 010203df7f6c6ec32fb907ddd1bfe342d0e638248cf885e02534d40ee5b0fdc7, 7f25490bb25c2a15a4ae4b09834af66b32f6c747cd410f205ebb5f26157628b3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5475 -3, 3, 072fef44ac2f7667c366f067b374016bdd39581dbe8acb0f8aadf98502d13646, 01011fba8c525ade36b44b91c10f763cc452b9f359138a17b7095defba3de81f, ddbcfaeaeefae22924db28ad1ac6dbab0878009449fa125d1e072edfd29bfe29, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4337 -12, 2, 2814b082c6683e9490338df3e42c8d89ca9e2f889735013dcbb7ddbdf7d846c4, 01081ae266672ca194549f553909f744cc0043134958ecc17e1cb23b8b321b81, eabf4471ea17eeb0e6b8ef2868bdf533e538e9117d89ee7be0506b5b87100c31, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4862 -13, 1, e36507d27632e2d6cc70480f6322a4469d20773ff0d047f6f70285e9d44beb5b, 01061e74c28eab539b083cc334b3d8f49d500d3a0efaf0ddacfeac3c3d7db59c, a1f90ec26c1e922437b5dce2cb7d5aa74a73dd6b2a633e0aa1a87e181ef4e493, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4183 -12, 3, 2814b082c6683e9490338df3e42c8d89ca9e2f889735013dcbb7ddbdf7d846c4, 0107093cee10017f16cccbff4c1bf264ad41d8416654ce13f71d06d40a068d45, 4728ca3016635dbb5fed672ad264689e97f39372d87fdc8751d24d305147e50f, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4808 -15, 0, dac78edf70e526e26a94ac4982c2abded02fe049c5038196738d66d80849ca3d, 010a4622db8bdc6f2d2a53c4d093a7db2eed1511435c0c9d5f3ad99ccc89461f, 782f1d06c5a143065b84a674c8c957ec1f6c335b9b9d1818aa440358490f9d42, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5165 -15, 1, dac78edf70e526e26a94ac4982c2abded02fe049c5038196738d66d80849ca3d, 010758cd460961f2865307025c35b5c371a152aa887aa79a5ed5db1562554b51, d949da8e7b84d066ea4e0d44064dfc4d8c58b4c4f6580a817de564a74500ce2e, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5165 -14, 3, 8c2cce72fa295e618dada27cd9dce2a8f24a4596d676edf11e096bcf6f5c078f, 01051655e2f51a514371801ef25660a391477f0d6c492b613c5a83bb30f490f6, 66473bfc883091c7d6b1ddcb8753db715e7a9568aca4b2f870f155edf0b952b3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4839 -15, 2, dac78edf70e526e26a94ac4982c2abded02fe049c5038196738d66d80849ca3d, 01034a2930d5d739d27f0a4b65bee1bd19ee6dccc2da5612aa3e380d60e263a3, 61cda7fa7dd9cb6b817139a51b7d17f3fef1ddb31db2a506082cd49411d0df57, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4541 -13, 2, e36507d27632e2d6cc70480f6322a4469d20773ff0d047f6f70285e9d44beb5b, 0102482e077b5bbcf0c1f5da9a77803663ade42ccf6667a2d07366ec25c05373, 7cdb704cdc5df918699d7bd399008f249f79ab8b4ca08689f8d3010c5813de38, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4183 -15, 3, dac78edf70e526e26a94ac4982c2abded02fe049c5038196738d66d80849ca3d, 0104501feb2927fd13760f5c39dc277827c125b069804c6ac18e6021b1811868, 61078520e97db6e828ce6465404fe3456dc27deb16516cfdc3c34c8293c52fd0, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4541 -13, 3, e36507d27632e2d6cc70480f6322a4469d20773ff0d047f6f70285e9d44beb5b, 01024f0896557a0084a1d9a2937130ccb2711a8cb810e512ec9e2c97c3c56db6, d4cbdaa2fb4e3c1e8ef809ed628f0abf16d3bf4a93f81ce03bf77b68e07fb516, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4183 -9, 0, a14d543d43592ce38ef38f600a2dba801557fd924638de966d2725dabc5e2472, 0102152a605e81991fe51e4c8964a98462f6a78ab03dfabfd30552d39e9d4eeb, c17c5c2242fd08302a00dddf39bfe32952a319f4816dbc540b991d8ee33180b4, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5099 -9, 1, a14d543d43592ce38ef38f600a2dba801557fd924638de966d2725dabc5e2472, 01012c56d755fe8e1c3c18ce6c13e4fb74a62f4dbf6b4d88a4d4d5f12a82111f, aec69cdd848a1b7ff0f89c9ea97feb759270ed6719ec8a675ceba9cc0877a1de, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5035 -9, 2, a14d543d43592ce38ef38f600a2dba801557fd924638de966d2725dabc5e2472, 010918e449d18c7ea382562025280bd33f3dea08e299eb92eaa7413d9b42c367, 1cb9b9ffb834abc7b8bbfeb01e006fd3cc412717f8b9456e5e4d663466417e99, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4281 -9, 3, a14d543d43592ce38ef38f600a2dba801557fd924638de966d2725dabc5e2472, 010a107f47f98b2357d358f56f2a9cb9759714f3bec8ae28dee41e9ce170a5db, c6d51cd991be84979a28aa4b4e51de3ef0f585aadbab7b66a0593805a5085546, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4132 -7, 0, 158b59e2f807c4b05160ad5dae50f96f079b79a7f7467e11d86c37a808515c1a, 010303ab82e286a8adbe84e8b7d72f1c627058d749eb7fee957227ae354736a9, e1b75477f812f034862401a8ecf7c9b7ffed63e904ebff99b4662bb3157d55f7, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4382 -7, 1, 158b59e2f807c4b05160ad5dae50f96f079b79a7f7467e11d86c37a808515c1a, 01061f0f60aa46bb3d3db810b004f0d63a0666f4304c7902c2d0de7116fd4c3a, a6453d96ed2963d2f8073faf29405223c3b9de7b9f0bf925ad5b5807f0df29ec, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4382 -7, 2, 158b59e2f807c4b05160ad5dae50f96f079b79a7f7467e11d86c37a808515c1a, 010a01e51b9910f599cf59ad2c563f0dedb61dbc3c0226bad673b48d1b538419, 1485b5332779be0d64bd08c69344580358b320cf01b9f38ce70badc069e94ef1, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4032 -7, 3, 158b59e2f807c4b05160ad5dae50f96f079b79a7f7467e11d86c37a808515c1a, 01050c4baf9613939235ebdaa17d71ffd8523d368247815fb47fa0fa6334e99b, 182c36a0398009d6b6e58301e13c805cd5acfc37412f55cadee180e90b27afdc, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4032 +5, 0, 39328e676b63cdd3ba4473d0431402f37b8326f6bb071bf52cd760a128f19013, 01063d6e0eeb11f4b8ee105280e1aeb4d5e280d6ec825a971b0815aa1dffb1a1, 3b61511981cbe61b4c5e26863eb5407b06562960bb9c1491039c7d45504f4a7d, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4320 +5, 1, 39328e676b63cdd3ba4473d0431402f37b8326f6bb071bf52cd760a128f19013, 01093cee5c710d8760b3fe828149c8726e2b9526d2ca5ca94e57c9e9e5074803, 5ffc51127e569a1f927fd10c6cc5e1f0eeee7540b922298b2f0bc61e37060008, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4320 +5, 2, 39328e676b63cdd3ba4473d0431402f37b8326f6bb071bf52cd760a128f19013, 010837ec9b30eaf304d315bd3bef0da838c0c9aa1f4c84521a565f79f093e54d, d40339a95e54d68ffc5574a246e7a5158af5c86ca17ff30d4da101819eeef7dc, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4320 +5, 3, 39328e676b63cdd3ba4473d0431402f37b8326f6bb071bf52cd760a128f19013, 010361d6347e3c2b08bc36b5bea2a5c93424c56575934614770fbcf0026adc91, 924b7a9f8736d7550c8cc75dac1d00a91677f67e653d9b37603f5e89873e4e2a, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4320 +10, 0, 538fc8ec58a371c654ae2369acf7ff0d8df7a4e3ff178c8d6960edc4bf7d8506, 01082aa824190e5d3c70448ad8bb4717bf35149597c81e4ef2afe3636ce69208, 71c696a563f40ba722062e49d4a44054f51a16039ce71d36b0dc4d7f2fa21616, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5863 +10, 1, 538fc8ec58a371c654ae2369acf7ff0d8df7a4e3ff178c8d6960edc4bf7d8506, 01022fab9f5bdccd0b959ed890ece6faba183333a3cc575641024319877396a7, cb4c9af70d7b2e076091c1a36f16933dacd55fd3be92403b5384e1300e3a6bb7, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5597 +10, 2, 538fc8ec58a371c654ae2369acf7ff0d8df7a4e3ff178c8d6960edc4bf7d8506, 010539217d7c1ec6bf99027cba42a6ad1925a721640dd971a9c8df0c0265788f, c12696a39fef53d899c413ad6241313794b50ffe873f2efcaef9515deb9a2601, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5055 +10, 3, 538fc8ec58a371c654ae2369acf7ff0d8df7a4e3ff178c8d6960edc4bf7d8506, 01050c5d9d85cea75c933969196e0f01bbcf93cf5efd6fcb19e19b766f92b2a1, 23d37b23fbbbe110e7bf9895c93a1632a69f37b5e24b294b0a23d19632d7571d, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4103 +11, 0, 9b01190712cc2b617960429079e1e99f5fcc066f89ed4d81e83c5ae60142bf11, 0109093319f061ecb6e666823ada8b4d0b2d874602d82ee8ed3e373c6cc56217, 49029a0333b014053c3f1f821e2c51274e03ad031c3f9d762136e10e3d251358, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4110 +11, 1, 9b01190712cc2b617960429079e1e99f5fcc066f89ed4d81e83c5ae60142bf11, 01045c6f75ff26b547da8b0e3e4d81f7789035b6139f2b53dfbf9261e1505e8d, 69e2db4bbbbd538f4b5c6de70837d155f398e876d95b5cd1c6da8341f24a039a, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4110 +11, 2, 9b01190712cc2b617960429079e1e99f5fcc066f89ed4d81e83c5ae60142bf11, 0107083255da35ebdbeecb2038a8fea675742c0b5b016de71b573baa6308bcc2, 5dd0ea87019b456f5d23141b9e5c9422a0618ea102ec97b237efdee486b7cca5, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4015 +11, 3, 9b01190712cc2b617960429079e1e99f5fcc066f89ed4d81e83c5ae60142bf11, 010735110790c4eed4fdb1d41f8bd3a28667d59d7eaeebd62aa738015fb59eed, 58fd1aa50da65a2169b6cf0742ea569dc3c7a3f1de9c43e5f73e9c47a82c0a3c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4015 +0, 0, 4d6951e72244e6d70bc3b0b6a6550ecb961c97cacd42e6aad2ff90edf065fc6b, 010643f21a4e13e83868b4ac8160fde045a61539b8d43df48f1d3c28b1b10ff3, a346ddbe9c5b2f8f432c4e734904657c1a043a45d2ebbc436409a50f37c7c990, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4024 +0, 1, 4d6951e72244e6d70bc3b0b6a6550ecb961c97cacd42e6aad2ff90edf065fc6b, 010a144bf3109ad87a0627dca4ab50ef1b7b0b6f18515eccb4dc1611dcd81c47, bdc015579ce60294299d6eac7e99a86dfada3e1b4e29e13920acd7a40e63387c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4024 +0, 2, 4d6951e72244e6d70bc3b0b6a6550ecb961c97cacd42e6aad2ff90edf065fc6b, 01071fe11e193c2fcd0947988b4d125b5397d48c5776def9fcbd56ce6f92f49f, 68ce9b1946965e6ff15e631a3792fece82cc2fcf8deaf7561b02664de2195e51, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3990 +0, 3, 4d6951e72244e6d70bc3b0b6a6550ecb961c97cacd42e6aad2ff90edf065fc6b, 010455fb5fd6d346f2c74b612072ddd502e7a17d433ba537db2da96e00cabeaa, 03cdfd62beb8c9c4fe9805a75677ec4466cf222e73923d6deccadf0174f14edd, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3990 +2, 0, d269d3c9e83f250a5d471d5d7c3f2b8c068728600216cef27524b3d1b6c5b371, 010428f05fa25f9fe8dad343ca0a2c8f64049d8a9f7e38e033130f3e0d847b87, ced7587ab3b4f773e15289b1ff5e656642cd551d431cdc9c847ff378a88cad36, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4366 +2, 1, d269d3c9e83f250a5d471d5d7c3f2b8c068728600216cef27524b3d1b6c5b371, 0107319e04b5dc666cff589612f47418fffa5537fbe829e91a1a47110b57559c, 564c0ef36f9b208c1a954328e7f9a3f54ca423bc73db67a4c0d5f3c90547cdee, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4084 +2, 2, d269d3c9e83f250a5d471d5d7c3f2b8c068728600216cef27524b3d1b6c5b371, 01085ce85cdb2f9a6316f90cbe6b7b79bd8e2cb69f27a48f9eb48a2c454082ce, 0fc40360c900954f92a6ef47ae075ab204ae06d7ac5bef846cb99daa4727065d, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4084 +2, 3, d269d3c9e83f250a5d471d5d7c3f2b8c068728600216cef27524b3d1b6c5b371, 01013f78fa10e1bf931096a2a8df7960b9de2f6b85a8b7e1cca8bad5b4c13d94, 7f04fc765ff3dab3d2148bd9d445c72370ca3f335e2551a742a911d3983a6558, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4084 +3, 0, cf0f607131740aec1059970e2de315626ecbe9dcc9b8e7fd03a744189a969fbb, 01061c061e969aac1afe128561b711fbf2f3462d407a0db322a63dbcd31d47cc, f29c2b98d0c2008b9c1c17bb491eea892c3b36d6913823a562d53f6c8f8190c5, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4151 +3, 1, cf0f607131740aec1059970e2de315626ecbe9dcc9b8e7fd03a744189a969fbb, 010708e6d5efa482aa0eeedbb5195fa33e0cd85966b746cbf4368486523b3d9d, ffcdbf851bc811fefb8ed19e030c45b22210df3de3bbddb0a119db24655f40b2, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4025 +3, 2, cf0f607131740aec1059970e2de315626ecbe9dcc9b8e7fd03a744189a969fbb, 01040424a692fcddb6f3c89680e02136ed8354bde8e75bec98f5944eb90e0b8e, 90e40d22ec747058cadc0d3d78bf37ea55a9efc17f5a9e9fe9331633283b832e, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4003 +3, 3, cf0f607131740aec1059970e2de315626ecbe9dcc9b8e7fd03a744189a969fbb, 010716ba575d16fd951744d958c36c0c7868c413a8145d8021f02d9ac8d18ba3, e8005fe308a6ca1000a38daf4efcc6ba4115c37fbd2413a93a216e2ba67ab94c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3950 +8, 0, eb1dd1de89c1808e995cf5ec9484ce99a44b6beef8f5696451eb45d1979a1afd, 01071074f94c7b46b48a562ef152b999f9bbd66885f30c862e67a1c058e12ebe, 3c9adc3aa8162877139de8552efd270d501866db15115e472d50d257497aa690, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4485 +8, 1, eb1dd1de89c1808e995cf5ec9484ce99a44b6beef8f5696451eb45d1979a1afd, 010a409e45c1036344945f841fecc19b5baf9b52b9e38ab1a53095749554dbe7, fa885262e5ac6af13bbde9086829e06b79b139516aadec74863f226ca63b2372, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4485 +8, 2, eb1dd1de89c1808e995cf5ec9484ce99a44b6beef8f5696451eb45d1979a1afd, 0105085e801eec72f2c302fd800d6055523c14de552532999b7e7ff148042cc0, 2d807da12bdf39158396cb94db480dcf875f3bacbad8193d6f94451198129584, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4470 +8, 3, eb1dd1de89c1808e995cf5ec9484ce99a44b6beef8f5696451eb45d1979a1afd, 010a0beaef18d340a523a50ed3803cb104c2c17ff19bd130d55908f2985fde64, 6886d2f62b722505da3c028aa52fb0e5b25d080d4babb38dfdd7cc7ffe1feeac, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4037 +4, 0, b7616f473c023eecd7d77b3562cae096fe5ced3e616aea14824a9c608b108f73, 010507d263112a994edbf20868e6d37bd3fee9d39900ca36e40b28b28a1f4722, bdb772d32bbc40e208a0b64780ce4029e024aba2334d3577f1af2741dafcca04, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4296 +4, 1, b7616f473c023eecd7d77b3562cae096fe5ced3e616aea14824a9c608b108f73, 01053be2154acdcf30778947d25c378bcaf91c5db0253c2e5dd488122ffd81d3, cb35fca83b2b502a30e080815b7800889df8485656be917bdcd504ac2d236ffb, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4296 +4, 2, b7616f473c023eecd7d77b3562cae096fe5ced3e616aea14824a9c608b108f73, 010707cd33ecd3a3a06b0de75b57dfb7055a4186c420aa6019946bd8d6729114, d6f0e8c19648292a186ebafe8585f4b5e3041218f16c5c0e78a88d900b83ea62, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4296 +4, 3, b7616f473c023eecd7d77b3562cae096fe5ced3e616aea14824a9c608b108f73, 0101274ec706e0b52f3ea3e663184c20ca41e72ea769051100d24757c922bdcd, c13078cff28f022685405d7460b916b45d880484a138d3a05e8cf9d0b86340f6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4296 +7, 0, e097286c70975340ceaa8f469be4ea733b00eff7c9fc94baa92d1b6c466f3d12, 010511b7804235c69f7cf9279f433479a9793d83367390698dac95197168d502, 30ef7c37d83e5709ad6724b883129db22f9c7e19ab9ea37e2dad50e46d0f325f, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4093 +7, 1, e097286c70975340ceaa8f469be4ea733b00eff7c9fc94baa92d1b6c466f3d12, 010945a965acca76c03ce5ea9e3a4c0ecb16dbbdc7bbf156dbd4ef0801f34bc4, 86dbfe6a602ee07d7e45aa4227978a0c2dfc18570df92568f07863c1e6912242, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4093 +7, 2, e097286c70975340ceaa8f469be4ea733b00eff7c9fc94baa92d1b6c466f3d12, 010525f4baea1a21cf4e44914e93553e558f24b866186ef7f7f15145cdfe3401, 376b4e8d2b7110d20c0fdab95e491529757eb706032aaa80b7adba7726215f77, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4090 +7, 3, e097286c70975340ceaa8f469be4ea733b00eff7c9fc94baa92d1b6c466f3d12, 010527dd5047bee95ae94b75c75ad22a200aab49c66bab70888c0257b7268d2b, 4e4edc07ff71d34de5d56352bcc4befd03ed5b0703d8b977ff3ecf2998722ed9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4083 +6, 0, 3ffa69241233915f4e17236bc03b30ad5f49c1b0630647d9548867b23d915ff7, 01061fc31fe4df546a8e9a11045a2f89c0ea0b9a9f7fc4737b6182bf5f32baca, 80ded011a9caa347a6603f224e6152b002fe2bfb7bf12f5b9a72b78010f9fbea, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4990 +6, 1, 3ffa69241233915f4e17236bc03b30ad5f49c1b0630647d9548867b23d915ff7, 01030bad30670ebfbdd4433acaf8e0af5a2a7cb682211761530d17ed3bcc1249, b7cd4af3a75eb78a0626c76b25b5a4b1be566bf4a9f8d351b14d5a3509e710f7, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4007 +6, 2, 3ffa69241233915f4e17236bc03b30ad5f49c1b0630647d9548867b23d915ff7, 010427015a5d4dd334101291097f7a303b05b86181f621179cab664acb041ad7, c5c1aef28e07f52345152f892f50d5f3c3533bdfc7d8525bcbed141513f3b9b8, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4007 +6, 3, 3ffa69241233915f4e17236bc03b30ad5f49c1b0630647d9548867b23d915ff7, 0101419bd618e12b74dfbc0257842ddd87099cfc6b226e6c28eb411f7de83653, a39db8178bef1aff2f85089fb24cecee189c414202297d221cb0fbbf238b882c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4007 +1, 0, f55edf0abe2efa13b69b4d0b68781f79a1fcc3670c9170ff0c461127da9b4a59, 010431e438eab93c4be3c9da56724076726933bdbdcca30b59fedced0a79e998, 8881e7fe8fd1a2e1e08ce9423699bf0bb6997bea2d8763ab51e09497de74b29b, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4298 +1, 1, f55edf0abe2efa13b69b4d0b68781f79a1fcc3670c9170ff0c461127da9b4a59, 0103101f4398732a849fdd717c7304d205bb8b54f0042fb21a25aa4d6ef3472b, 69b7a6a64c54f6ede93f291feeb6f481e9569ae79c426de617434417d802c0a3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4225 +1, 2, f55edf0abe2efa13b69b4d0b68781f79a1fcc3670c9170ff0c461127da9b4a59, 01080a2b54018c61e7524856e090cc98b2f7713babb7082a9c125c2f47a84104, 2c666088a7713615e08683bdbef7b17764522eb2ab699d4584858739ff8154f2, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4086 +1, 3, f55edf0abe2efa13b69b4d0b68781f79a1fcc3670c9170ff0c461127da9b4a59, 01032363a6b032f025e22dd96d946c4c77ed741473d19d2f998bca13e80ab21a, afbca6ae4902c5a4d894acd25d57fb91577699132cdd4ab65633f1e07117be4c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3921 +9, 0, 778a5f9b13e6754fd05684f2eaafb093794ab3e864c4ef5c5a30c44b181bee0f, 01072cc7ed3c8d4511d3df97f1917002650b90dd23293df7b9693c4b32953eb4, 338df6d2204ba7c38dafbc5ada86d165c003162501108156ea530d7b3f4465cf, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5258 +9, 1, 778a5f9b13e6754fd05684f2eaafb093794ab3e864c4ef5c5a30c44b181bee0f, 010906bc116085964f6e05b17d9ced5d7638dd800f9b9f491bb62cd22ada4b6f, f2a21fcf4c55ec1bcae0716ea793523471424cce625cbc2b1c62194a46b9c763, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4607 +9, 2, 778a5f9b13e6754fd05684f2eaafb093794ab3e864c4ef5c5a30c44b181bee0f, 01030d703cc5730ff14717d864b1e50eccd7a7fd180cff06d233e8acb153f05c, 796a83a0142df9652b530143f33d8cdfce181d8c7513beef9b9d692a1c50d58b, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4252 +9, 3, 778a5f9b13e6754fd05684f2eaafb093794ab3e864c4ef5c5a30c44b181bee0f, 01023d40919862ed174aa0903881d2de7dfc1741a86b834a0d1db36ea5ba0476, 51220ac298d1a70f3496834c6361a82e818c71560f35ded4730ab3bed3b34ac3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4252 +12, 0, ffa831d1e5798862fee1146cb9e7cbc23452fe633fb16752f99f32d7b8e8b5cb, 01091a674dff91b7c38f43c9fd03a6124bbed9544c301ca78a370fbda290870f, c5dc870536bd345374b2efecb2044ef9f1492d5a563d6aed0b2a8b33d18caa5c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5182 +12, 1, ffa831d1e5798862fee1146cb9e7cbc23452fe633fb16752f99f32d7b8e8b5cb, 01033aead48b1256cc6c2db85361c0da448fc99ae278322cd4349336c672e330, 8499f3a88422c7a4469a8c6b12ec73a569665ddf435f0d2f0fcb4b9d123626af, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5182 +12, 2, ffa831d1e5798862fee1146cb9e7cbc23452fe633fb16752f99f32d7b8e8b5cb, 01034f81a9323a3fe2a4c4586e40e9cfb3a144d7b7f15038dd40426bebf4547c, c3fbe32d39964bc177b563eac4d67f03a3e80b84aa592d1ca8a8371bb622ece6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5154 +12, 3, ffa831d1e5798862fee1146cb9e7cbc23452fe633fb16752f99f32d7b8e8b5cb, 01013f9bb45b3c7c67233d185dfadd279d75b79e0f9b40ff5751433ba0cf80f4, fdaf0e3152166a8c0c8ecea4215f15c75d5286d912aca439ec126ab589ddc007, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4634 +14, 0, fdca870684bd40b126f01c964f68b0518b5b5e971bacb418cbc256a5861fc038, 01011f30857cb23af64a5a0deec3c40ef9cf45cb7f6ed8ac3f21a1c892786b2a, f78d2b1033e1f2382a833b3a4c61d9c5d2e5bd1dfa075d2e691dbb25ad3b3c98, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4526 +14, 1, fdca870684bd40b126f01c964f68b0518b5b5e971bacb418cbc256a5861fc038, 01091907cf5e956040192b5db3a5edbc76c1bb088b9a77cc13490a1daeb6e977, a77c9d981f188ec8411a3d0a72c96922167c9fbfbb6665de16c7c57afb6e15f0, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4420 +14, 2, fdca870684bd40b126f01c964f68b0518b5b5e971bacb418cbc256a5861fc038, 010a08b37ab285018c4ad1082e543164c2b2a13e20c4acce029604ca9d2ffb22, 2c1285b6b4a12ac7fd7195fae65371d7da402a7a4f8b10046ff9cdd1be1365d6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3949 +14, 3, fdca870684bd40b126f01c964f68b0518b5b5e971bacb418cbc256a5861fc038, 01013ba097ecdcafc088477791d2024f48b247bdd0d87d163803af5d22369d06, 9bad023b69cc8ceb4ddec62732c460c967e36265d7fee97bd1f7f6ebff089ee4, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3949 +15, 0, 287b689730e2bc1225674d3640924dcfb5133be02359cc8d14bffe70b1bf159c, 0108406db6989cf01571981674520b7362fe944c4e0483f1052f1b6422dbbf8e, a232474b89fc06f50bffe99663376e99ad545c211c438f84a2248a55894b3278, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4640 +15, 1, 287b689730e2bc1225674d3640924dcfb5133be02359cc8d14bffe70b1bf159c, 010536142e58d24d2a46c6c4108b1ea81eb72e381d2d0a11501747c6e59fe2e4, 5cf67c6acf27e226720170354cacad9f451617fdc9cc2c78155d6880ee42050c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4131 +15, 2, 287b689730e2bc1225674d3640924dcfb5133be02359cc8d14bffe70b1bf159c, 010822451956749b7695f5ab431a90e7b5e6f12cc4230b8980397cc119b4f3d6, 5124ba73fb9444bfa2158c2070f271aaf63d46b554f42a6ef1014925e031b514, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4005 +15, 3, 287b689730e2bc1225674d3640924dcfb5133be02359cc8d14bffe70b1bf159c, 0106086b217b66240fa9e86343d45e7c2680e1922aef83ed1064045e180d6ec1, 50e38e423d762b4fc02723fdca764b50f51b6a700993b56a1ffd7e43385477f1, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3913 +13, 0, 5f10c44e7cc47758dc8ed15da928d398723165c11ad92e8e065892b0c5b1ba16, 01061296db9d8060d3cec6aa3013d3236e9f524dd07e2a4b465c857ca1095fb5, 22ab32567bca60e8672429b7440d3a5658c1317be299212c07b28cc28dacd0a9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4173 +13, 1, 5f10c44e7cc47758dc8ed15da928d398723165c11ad92e8e065892b0c5b1ba16, 01073743ac0c235540589c2a8539d8aa1e35dc9cc7135f00bf96a3bfc93b8f4b, 9da7c23a6f60904b97b39f859daaa2cc154c7b60b3752a8fa66a0f976a579220, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4173 +13, 2, 5f10c44e7cc47758dc8ed15da928d398723165c11ad92e8e065892b0c5b1ba16, 010806ebf3a372c7dd00bfa473500ce244254c8e4b91ad48a19c44a73e8002a6, 21b56be5db094d08d47b64cd94ce4031b19b685bec283cb3ace2cd7f1c7a7272, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3974 +13, 3, 5f10c44e7cc47758dc8ed15da928d398723165c11ad92e8e065892b0c5b1ba16, 010430da00fe2903025447bb3862843d6e2f36728b9b9a7a85875473811f77a7, 11eada061151751e0b82093b39d3b35bbf395b22f847da6904f38aaa9e375f6b, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3974 diff --git a/test/data/gt_production.csv b/test/data/gt_production.csv index 80569d03..5608faa4 100644 --- a/test/data/gt_production.csv +++ b/test/data/gt_production.csv @@ -1,97 +1,97 @@ pubkey, nonce, miningseed, score -039cc1f1560aa96daa994a2b296f22d7f2fc9503ce95321d0b8193079e5f93dc, bee1e55fc2c967601827dd80f09a6b0a47ceab37b920b57efbd23c8fdc49e38f, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4254 -9b3d041660f08f692ae1005118f85c35bec491c33b51c94ef126663f4402fb1e, 1a314eb596f55aa79e7f61334072380042ea4e351ca59dc09f536f8224a39afa, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4787 -5ddde3533e040840b737304dd92b4b48d62ab209419a8bf4506e7b0497e9c614, 419569517e0908f9bae5b07159d322204053afd4564b4079fe160f775e214993, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4329 -66950904cb50245726a50f9039e2d384bb878ce3e39e0575f64eb61bf9963325, 158191c4be8a6382d9eff13a448550c7742568909abc76d8213c958e85bb02f5, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4021 -beb0cbbaab3cc54e87e4b6ed4a5f6c4af2fa4f452a02252a8ec910c43fb71a43, 96725650ea0a2052e3175c0a1047f51fcc7031f1455fe168630f0ecbfce1a180, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4642 -c7f5024050c450389d341eabcea04b16e79b7a51f18bcb59284eaf40000cb7a6, 77b3965cec126453cfeb23b5b44f20d652058f7726f0e43e8c265eafba6e8418, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4138 -a6f49eb0c3fd08eeb56b4ab83bbfb0b415b02233101baff5eec26bb2d1455e34, ac8db67e608af98f13f70b611c71f527dbb626afe41128cc6dbb27da921a6db5, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4323 -5c49beb4e29c921d8fad67d9b1e09fd693254f9275dfd77ded5a66acf721b4fd, 5e80e9f095fadb379b12130c4796f74ec967fc526b1face4af813a13b029fcd3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4596 -d8470f83e18ebd6bd221423f03b0e17a87872f6ef25ca8909f28078a6c300ff0, 7989ab5f03db1a93b51a8629ac6d569ff0299ee1744ddce813013e9a64ff6497, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4254 -4c83417da5f166accd29107292d5c0b7502811b4e0eef6d252c01474dce157bb, 59245fdd6fb52df651c4b3228596175e327aaaf4f4405cd37bf84ac1a9e51747, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4476 -aee5c8edcf3dfdab7ec84b7c580be8a64c0b902be6b365fe916733f05e238ac2, 56dddea0c9052587a47db7f5deaa16727663725edcbd0c5d78f9b1193bef12da, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4179 -f2112f60356004b9fe52840e06e9047d2020ba228b7e1987663491a7d90e3e85, 4893d1aeb7591ce464e26ed43e34501acf450a6d6ff3a4dbb85a3920d9011308, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4227 -050a8c2acdcc99c71b1f6e04f3785291f2980fcfbc0a2355a200eec5287a18d8, cd2744b32506fc199fb1aaf0b9edaa553d05f0f4c66e9f7d2e47140fbc7973e6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4112 -5bb6da0fd6e14135dcaee492185a223c5495936b29cabecf415094e1cf0b64a4, 1b169a9f717bd61508b009a9e149c3bf3bf192acbc2a11af6da31214ca0c1be2, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4367 -ce103ac663da7295f0013f33b1eadb5295e32dbf8392f67b8085caca9d8291da, 44fdb9bc59d865bd9d87f049ea4a7a702d1a0fca20cddb7172bb10d7c17dcb06, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4373 -da39c1bc6effaa3edb88cc805e6e7bd7aad1726ade6b0dd38d54b52957f14125, 50ee325b50af29c82a0ac06b3ec85885f9e36e08f1342a938acfd9a002ce524e, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4538 -2f2a57841de709ea79ec678d1848a2dc13cb1d96d1d029278181e925ce6ce281, 29b9f9db6a5001480686637bb3f187868d23a8881b32482000a4e3b217074d54, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5044 -7bf6c1e4d203b633f0f5f5421d2d16e66db924cccc51092f1e89ededa54ad322, 490f7927f8f91dbeab6434e4f0ca660642d9157267613ded72a8315df821d838, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4272 -a6f0ae22305a9a0336a2c7d97e1568697cdb62e1aedfc9874a1ac1915b4e3c67, 332f47bf272e90de5befc330c0ae65b88a981c6c28819c9df8bbc7fccd7176c8, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5126 -1f710bfadc49f5a1bed75fb10054979a3b4856a48e3b75184f7b056d3a23ac41, 6d5adaad8e0ea9208390e5649d2e0c6365ee84cc1611ae5f765424d7992b8322, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4227 -36f621af4c726d3e5bc3cce3d26a4418c2272024fd8177110a6a64fcc9071afd, 6fa998c1ee8efba299b8b51e865a13b939df3189d94c0b939f86c8b2d9fb7893, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4002 -8117882a9cbfce4ca0df28f72a818773ba80377417e8c73d2c0a56e8c6e4ae22, c762e691c4918cc826e72b5277fa2c11f4ff9cb2a01efefabe6ab8f6c8c5be3e, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4124 -a7a69484aeb270fda2ce28d9b6d4ef6f1e73edde9adea5404f81b7ae265a24bc, 4b18c6bf59aeaf1f96a11956eba1c2ac7bbddbed8e0f12ec5a94dcc8107a5cb6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4500 -a84e3c009575dd77299c18d600d0252f9d0480d34c5da189a7625a0274f83cc8, e0c96c11ba7a7137aa7fe213dd34dee838a598acf0d76776e5c3778307c1e631, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4334 -dc100c8d058c0cf6df88dbf4fe01f6023b173be3bedd6b86dee4b8eecc72e058, 179fdf1f611d6c27a08eb533b826d169a0bd3d33e6c329ded1eee76fcefcd2dd, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4259 -087fa2a7ada94299393d118b19c6360699845db46f559eba3ee5b8bc494f2a8f, 0f200d7799483ecf52a4af5c3a69111fd29a14bd7a03a82f9f39f1913b29e5f8, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4347 -8f9069a8eb962dc47afac9ea7d4a77d7ab801c3e932183f9dcda9c8cd7fdaf9b, ab4f54740589f62c4e35c2b7a2508a3f7fb23872afd62771e57deea64bc540d9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4477 -ecbce54faf01fbf9015cf9b19be9bbf42e9530be81fe09fc92c666eace8bfc15, 86acf64f19dc27151febf20c2549d49b05f4dc87290e108e935ebd6d49950cee, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4513 -36e28fe48780115b126255d893208b16f04505687e9aafd17ba73dbe543a8319, ab908ba2a064b7381f26c7a02cfaa2d8b7689d5f24877aaad99e25e0714ca19b, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4711 -ee4fca45aa951e2a2c0e535fddda71cd1c0722f1b30bf74416d9ac6df8787e52, b53606d34971e204c260e1c5c2bcb88450f429b76340d0860046bf2a67cc359f, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4634 -25b3ace18ee066c5cb561d32bfc89f69f20199741519d148a1d00312191e1f0f, 06ab93f88f0fe2aaa54efe1abf60cceef2b21d15c378d66096398c8b82947c7d, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4032 -a7149ab5f6c9f3ec2dc997564839a050de78dcf54e255d0019b4d81c3675bb56, accab93bf12ddf0a8b2ee43bcb94fe01d265eacd6df6aa8bcf133a5d359a79f6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4964 -d4aaeaf020007d1349590d77e2f779ad48f9a6cf720d04ca6a65fbbad81dc050, 7a40d7032c899ac35f224bd0a7be2c12244e9b2b4a32852b2ec880d526929ece, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 5357 -5fd98a26b2bcd498daef005d5035336777164fd594690d083ffc07c2750a00bf, f8c4c01377b01fb07d914d37301d2eee2349e015128adac766c676913f7c056e, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4538 -acc5d85d4bdb5d3e63879ee9975db8dd89474eaa143fd3a3b0dd9b406e6fb19a, 2097e387f6f27d6f630532f3407189adfebfae9bfe4308f0a4a718b72756cb64, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4251 -5329b924cde1d5be74ae7b98fd5dcd6c6cfc497abaabcfd13ba6ad76b3e80a73, 409da8d39041e54f20851bc0ae095cb8b80ba393af8531ea6f1977e14e900bca, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4435 -bd50121a1a9982b2e6f1becff5beced13d3aeb3b8292b6d64eaa641a85c6af36, e2c0de9cf3488fb4db674d9b6bdb7261044556a5ff21d66781c975316624ce8b, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4064 -f09fa2dd45236ebd36dfca4443787119dcefdd152723cf5f4346f16c33611ca4, ab4f459cae0455dcee65258b8c8340e3429cb43d826001a52542b0b10b2d3051, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4618 -5df6390b68e67e1cff09fea890e14b204f8b5e9ef847b14a05b7b32f83576c73, d2c779399ff772a44024e4991f32463193e7d21e958b926ead5186fad44d276e, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4658 -0c5c814529d514ed2084f87554a1278e303aa627c720c21502509e6453b46e75, 4d2317b4d7645a12954929dd48e1541ab6c333eeb590fed06bc795876d399b86, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4805 -1ec0460a6d6ecba87d61bde51c2b27c758cefecc568cd807b5d34c51d4d9f427, d0aaeea09c9dee00f7062fabcf47fd61f2c288b80295a820ec4222a18c2f67cd, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 5385 -e91795a4cc4ea7a134da695bd2ca1638e3510d2ed21a6207c34b25c920925556, 2cfdfbd4047379ca02f323115287c8132cbd1ab84ae94bb4142147ccfaabcde9, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4104 -547ec5ed8156d06f369b8642d81f542aeb06d8be0fddfb82d499722def67ab8d, 58315e20f5e05abcbb1c225435e985dc08edb01a51bb472e5a2caa1ebea54636, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4293 -d8e5cac99aa0e4d53d13a2a5efcb44533def7116cd8cab23b3e8ffe2e6e36033, db5d8b11d57d382bb4a174aeaeea1934089ca1ce87f695b02e969aa2d804323b, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4490 -11e6f5455923f425403174d26957f3c4343082676f2786a504b175a37e095257, 3d2b359ee811655af3afa43d4b6bcef022d9b086bf7b90196f8d2fd319cef2e9, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4101 -3c5904c5eef6ae6bd1c5f04b329bf8263f5de55ed27b416caec5d1c7ba8a1563, 4595621029e319dd6a712f40b4694bb41a198b97fcb5e1431e36b43d2a49f70c, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4229 -bd57f4064ecfd309f405a10536844d1ba5ee33ef367100f8a7bb8c0d30564c09, 3e3fe3eb8a8e57fd02bf0aed9362a8fa922792c23466f23910a07f5ecd6a5891, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4399 -8cd870e8a74f72c9293b449aff6f2703eee771a2e804ab3b0e85e90110a70ebe, e4043488f279cd9c732d5074e615a25c2684e84c82d86c742627ea7a5eacca33, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4427 -2aad58dee6a39af963379f65fe80379c9787fa0ef39ac6cfca83883b39d88f80, fe7024fbe4ed4989b6004ecc78ef92db203631b606d3bcc0266406bc2dc2c95d, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4211 -2944a74d098bc8cb2c49c7058fa5c1749b6b0762e7953d2909d081a621322bc9, a4251d12a25da7049ac54781cfcf729548fba470abd9aab5071854aa33651f48, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4392 -59423970ab4e01dcd75b21c9bb28039cc754cbd9504f1baf7690ad67f653d9bc, 8cb0ca3bc6616437eac5c040fcbc82de45a9374a686c68be73e1ece507fbe7a2, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4234 -4111344f7654a20f63f205251e233949ced281978307fe80f5994101ba30fff5, e0cb33733e52c5cfe2f342b834e6be21a4c8ba142184a5a712fbe01d2f321ca0, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4192 -cb2e9d7b14e92a7acd3606295181a85b3a3834812c85a58c5d8e8fb966d52e80, ffe82f2f47efff7e3c7c1f8489126be98246f789a0d5cff4e62e251d7e9637b5, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4246 -6acfa4e805379c4ad2c8fed920eb94bea5c66753f76c9e8903cb9f061b9a8568, 3291da84832c5e9fd2ec14ab4bc3852a9f501635962490595f92fccdb66104aa, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4277 -5950a4ccc1ee66af76e4ddec06caa7a0992cb2f59b7fe0468edd64f7bf7ac3e5, 8f6afeae5b25d78e7a9105fe200525e0012cd20ccf172dd0f610dbf804c1d731, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4140 -971e704c6ef92be3bedbdf77b8829804ee21011c6a2a3e79a68b1d4ad1cf826f, 19be39d709b2b1cd553f4ade9553d982a5ebc2028c7d61fa2693ce7c701ccf2b, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4122 -95ebe78a1bec9a12aa57462a7818c047b9cae0376666d0640905e81ff6e9e356, 7a418455ac94a63d6726f6fdecbf32d9426dd990ba0548e31e710703639980f2, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4418 -8782eea2fe0934ef90467ece2754218f16e02f731277ec7f244217f0ee0bb764, 7904e219e0265db109364c873d81f45e7e7522e881f9646faeba35fcc49985af, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4189 -7097a76021713afceeaa610bb79cffa218a8f6235ea1aefe078e957210170a47, 4068216c2a24e21043646f0f6b411628892c9350517451f528e632dc757ddabd, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4174 -e2c85076c6767b991f71921fa6e3bdd669a3e1e739385b4f0330250c828bf8ec, 017471a30b0129216db7a28a5da7dd2b16c4bc856c85d5135985119e4a99e6d3, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4336 -983e19c755f902f0026754de7fc89dbfba6ccc17f161a6f69c94e27ae5f3b8f4, 63301609c9ae6610af2aa1282c70531380a6d90e2b87d36ced6a84108f64755f, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4686 -66f9328e9c1a79595d52798a25d3459552afa6af9aa52ceb7ec76f38a8f3bd39, 0b71ad39ef27577b06bceab5f264201b2e8087143e0df90c69b0e206042470f1, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4307 -befe30abf1c5e24965d39b7ff3f2fc33fc4698be8a33c87aa4f8536ddb8a1996, ad87664522487ee7986f5ce7c26ff962b5feba171551b15b047d9a7053dedca5, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4235 -0363966c6d6ea832ddb1df165841a3479df9ec8d2ce0cea8a9400360f3cecc34, 391a80f81e72894605743026c249995103834322ce94fd953107a781583c2f23, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4472 -2cfd43630593e07902ff52948ce387aaf0e2bb0e11952f8a0c91a33672203bcc, 157ebdf68c866bef7f93adafa1490ce1c15c3b951f23461f5327b53b3546726a, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4874 -f99a06858d3d9dd741b0d1ce08556aab5c4b383b8199920d776daea35634b1b6, 494feb7026395e0ce15b603a2e424b79cea1d46da11fead0d84c29cae57299a5, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4657 -09bf69a84c357ff346577e5d17748c2b3d5b3ed84e9329759b131c2b94e7f4b5, 108075e0dfc82821119e0f323ab789ef809dc39b203db23ee3242f3a42ec0a80, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4728 -7ba937604ac1bdd1cdda1954b6b6a93eda265f468a49f4941f8de0980caed213, eaf8c0959fdd124b1756144818ba32a60f4fe12454bc342e25a74b13b745241a, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4274 -060f7e1a8c8cb6f938b3116ff25884b6eb87fafe9ec957d07ca28ee58fea68ee, fdead1f49e206d1892dc77b38a8b0fc8a286d01683ee994f7fe591c6f66b2bdd, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4212 -377741f3abd82ac5b90df9edf3d59ca9ab1fea5376dadb40242c87f55a6c287a, 9669b5d4bf3ef9184daac77ab44f0e3ce4e46752e2a7fd68ebecfbcf6109fe3a, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 5152 -7a4fcbb032e872b47f2a33fbb2d27fa06965165a91ce284a8d29a43bb4d118aa, edd6bd4e7bff26c7e441011422fcfe753bded55005a5221046884efb2afafaa9, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4144 -6ceb9db060f76ff7a170037cf3fb0413f3bcb5b2cbdf7f42106d30fe6516be95, 9c83bb2e063e73e7338944f8babdfd18d01ed1d13c9baa43fa7489fb52f176c2, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 3956 -43e55c8788cad0c02cdf142a7f7e4683b332ed6a8efcc831c580aab3e9a1b27e, d9990b01cbe2746aafedc17d14f5a84675938f952eaeee0b3b5764e4bf054074, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4236 -cd0e79b31ed4e699811ca0fcbaf81cb3b0e08188acfc38b6a98e3f6d077e6fe7, 3625fb00a429fd89c4ce1c4f92303cececfbccfd7bc10b1f00d98e8e8540fbd6, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4421 -61217450605f85dfd56bb5c1c046db8018bec5c533f3d5dd92732ffdf8786e60, a165d267e0fa10de03d6149b6853930ba3f9a244f3dd07a66b202f064535dc6a, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4330 -c7f29ca4e434a0283049a26b8dba34072bf7a90e2116a5374171b0137789fa34, cce63e8a939e4dbbcff17212af5f390f7b5d5c7ac7c1c4bdc9db353b324b092c, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 5074 -19c0127c42618672cda53ef8998cf7a79bce5eda48aba533c4c157db73bf1430, 63f09dee35a7ad227266f20295f406311a3563765e51dcab2135f589127c5750, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4916 -93970bbe1875f9bd9766a225f95aa3d3e748ae4ff7a629e8d8067582ae6874be, 4e9fc8003d5384be5dde14ed04c9bb256dfc34882ae7c51873a74aba28e300c5, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4419 -6b17204cc22a12de5171f59978a5598908a3778e9d635e925190e312f27a5104, eb631efad97aa19034488b9b19fd26ac670fb584ed0f7eeb0bff2d4f1e29f0c4, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4692 -d0d2996accf533fac438f273ab65edc50eb5a756a727fa9c51308f8b8608583a, fd0ad8a263a4cae7cc1cba87fa8c5af6b0c74694cf6dcc9c56e4867d1467b53e, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4544 -f075dc821b7f48bb638dd898b9c1336abe8ac4b8ba5c964b5900c8b26fb89223, 88ed66223b41c277d464e33e1eca9e022df91b476a316050c9fe296c2d27a55a, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4127 -c14ff23a8dd058c87e49edb5b6e0433e24238911ec1c7c179cd8338ad3f1eea5, c35e26a31a309a160cd348f3540aef3277f3ca07eb869c59634b6986f44ae8d0, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4488 -7baf3e8717e443d1f3a2459c6e4368b79a6233f75e236aa6367b5eaaa2deb8cc, 853d8bc62f96344aed6f69a683ff7bb15f82839fecadbbad8b2eeda3713b73d6, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 5043 -aaab6229bd572c77a7a89b3b9a7bd17cfd085528ac5471fc358b1a4c6d011c4a, 1276013c419de03614ac9da61dc28c732b5d706529e5492caab405db613d6340, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4216 -f73ab8675d3eef76ffd330ad2ef8c2dbdb2f5fe8e76bb7c5458639f2e2cdb7b4, 5c3dba3a97e7cf0d9b92e5cbb8228d5b6ea3f9dc54fba6c288e6181b03ba369f, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4014 -adede9708eecf496012fb63c6dba5926c896d077ff1bd2e8a911b4eced495d82, 66cbfcfbd434f0a4c066c6e42c54c8a5ff544eb81834c14781850421d3b87d28, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4549 -0c5a86ecee90b2275137aead69f1005ef9f1edc420845fa667ea020f98c8d064, b033d35f89a05b58388d4cfd3f2cb855ae4a3925353926b6f267a06d63fc9420, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4144 -f2ce52d93723e7c386d4604876a3c3ae7a06a295411ea2e3719f0d5b5e2a5e44, f3b1935402df86ee59dd2913a11fb327451542645a62bb8e5217a459e242e820, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4309 -f067e50164c5eab1194c3b77cc119365c01ec9501a7adf5becc7804c46695dff, f342f24d14e52fb7441f409386ab457e353caa06ee395beaa25e27be5e1a7c7b, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4517 -7a33c852aef3009f1a8096124c4e166115fd14c1619bf8a1275b94292787c274, 1f704e695d2b807cd9b3f877ab12fa6d5cabe77127a589fb2ae6df94470e7717, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4352 -34cd3c37f0cbda82af0b62b0b3c75887a1332c5a855218c75fe47e2d6bc6aeec, 35df8cd36c809e50e74990932a316fe5ecefcbb19fe3f4a8d027fff912202998, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4042 -dc679985b8b7d22ca9dcf52c947f826d9cf6f36582b70f7282ac1b67a0c46ae8, 80c2186977f4180ad43db66bb7d5596d7e42410dbaf7e84734b71d589f46724e, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4054 -17f3b29ae0d930361208d16169894ee4ed9b5846bd01147d0c7bcf05ae7d25e4, 5ef965812a3918a2dec24cedfb23a306a0305e40e52d9b38ff00b2a0d2ebe616, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4480 -836eb6226f84ecc4a270ea559f2b3418cdc0bb9eb66a26bcceaa7e45c802cdee, 9a7512dc2a55c148ee1900e85f9bec223bdd73ba25b650873433164d196f2349, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4849 -6e8090bf760bbd8dd5146ebe3420223b8be6b30b65e714220c27d1c2ab62423e, 29ad5c73e1f39167a370020bc2ed3af84e640bfcde167a6981f0114723fd7ac9, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4296 -9c256502b40844cd9f2b11ed9eb5472441731b66797542837dbc68ee2524cb98, 5627705fd251600ddff839d02077e646f85dbb08fc08947e9098d0e2c289022a, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4454 +5c6abfcb759cfacf849b67bd300b165b96084ca0e71176cfb7c5a33dc08bbaad, 83117509e50f639121de2895d6ab1be7414f6e4eb47a584b23c868eb02efbcda, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4834 +3e09a895669d7bc0a0178ee5a222efa0aadced0323b1d9979d8c91245bd2d807, e04b504d08bc81e18ffb0c5e26053179a02462db28e087053d0f190774d9ed41, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4299 +69848177b7d38c8dc9e4d19b7105ee6a5cb7fc2336a05775183343566ba2c96f, cf663581e3966026b8eb3cbfcca5123587eb54dc4be7840409b90f6b3bbbcf90, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4111 +43b689a5bf647c1be885a1edcad5b556c82d653af74e5b550259071e41b585a2, 2dd0f2368db7d54246c1eb6b25769cbec11afaa1cfc91dc2a8c8f30aa9e8fdbd, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4408 +7cbe7074fa28b6b729ae68dca950f3771d4d366ebd7143ba8e8c2760acfe86fe, f5797c25536802d6c8090650d59575863c26c838475ecdbc91f8acc86e22113c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4079 +528018592df920788f63e08e67bb28ec93773d1abaec524cc3e272fed477b256, 475fc1adc179f7d2863f15f0bc581d9028e6bfedd98f294e5bac85c833c0f146, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4332 +8a98ab944c81eb261eb5139fc559d38194ec8cebff331bfd8f6c3ee120be9727, 892ecbb404e49bdce6a635322ca7d0079bf1a10f70c1a7ba5a1ad5750bfab59c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4191 +252d81094d6463586ea147fc81b8c52f9c2c662b7b46a9ce7c24b40713eb23f6, 65c4101fbad875daf96c2336bc1765949528d7347d6d620530177d631c87f872, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4002 +b5222770b54b868a918dff38148836037e24c57205f69c05af18dffcae566caa, f96f40960247a776db986f1188ea577cfced37a8d3cf9d829c1fb482c694ff03, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4014 +233bd25bb925a17e74b57644d01a8f02aa190d5fc458a3e9146af6a18fbfc4e5, d970c14ae7dde13424696cfe13c2537df036ea1982c1265df7e1791d834f0d9d, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4053 +4abe622ff9ad1579f45bfcbe745fabf5b08feb40c957b3b3b599e3965865000e, d8ab02479da9e51d2e8e98248f8448fc933e586f1ea660e48d361aad4fc95efe, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4656 +1e2049d7f096d7139a9d0953e3de346e29d869e42088acfb0e105b1e5beec400, 50016a62dd87e15506817198a9227e1b4cb51f478c2cff5710d27424cf63d210, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5361 +b79d9aea466940965878d446c5afc50cca28ce50fcbe69ebada6b38c5cfca999, d37aa7a0ba4fb51628de0747ab5de0a76bfe0ddbdb930e0cc440612229b5faa3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4017 +3974d191cf4fc9b4c32997ef596817950c7c9a76310ae8a49d1a3340575fcbcd, a558d8dca066bedca197c2c0d5b77e55db6531531e0668434e8a9b7883fb18ec, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4088 +97683b0f82cecfd2fe4cdfdb2e6cfd7c1955ffeefdbe6a43b1d45ae623db38b2, d67466d62add63979c892058f3107b5f950fdc750558e679727e3fbf8c74f443, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4146 +a776ed3457761d8d5c3ccbb99b9bfc4c7c1c13fcc4cc00a885a36beab298565d, 36301c6a59665c4dec2cfaa970bd9f1379236601521c0e284cb859933b5bba02, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4005 +f219619241de2bedf3cc635d3e90a0c085628d0bf90d33a17bc49e1d59932b49, 4b234c876cdadefa0ebd99f6821a042f6973ce06771a31c00ca23d4cd1ad15fc, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4556 +bd094d0209ab1ea3b4d2f5ba0a17639deb046e5252fef7a7f297fe4e2a231bd8, a8915bcde2eed41171cb46b189fb2b5bb5cc55e769162a55c6489729bd6224c8, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3998 +1be2fe6b05634c1f4b2724b1731192edf9da6d82201bd424781581b840a74c97, 7719b80b4343d5538ba4d343cd408aa1327a3ebe824eea3162771c6d7041b644, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3881 +5823092689e26cab35c4548e187b27968db44ee2ebc9462e7825dc629ec3cd5a, 82b10fd72b1eee4e17647cd782d9b65b5e6dc2e42816f95ace4cefb85aa330db, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4266 +1a597194d68fd4472f1e9b0e0246048091a68f276edd7b3d098ec302c5c5dbb0, 722423c42f5c37c3fd546d305f7bac2a7b3b3d502e88f94dc9d777b4b0595ef2, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4196 +69f7f8a0a5785812a99987b520198033099c7711364131330f349adf84043da0, 9650ee05ea97d8dfe539e561a95d13bb0ed3010122cd83d7cb2de12e17117e78, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4051 +96d97311d817be86a1e92669190d61b5f74a6349300cf9866e8947ae2eeb8b56, f9d2d3f44d659b2fcdd1cb790d05e9e07ae8c02a639dc6fac51128d2e647adf2, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4846 +0fb2d3f10a46a8589ce717c9fcf2d0964a7587dad5ed77c5e9fd77c770e6f96c, 0aaf6507ae1b6741d33dcd9df870382e909968a4913ed120ace7e8a8c5db97b0, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4294967295 +ebf175e23874afbfdc78b553e2a18d6d437b1315791038a05fa27cf23eb43834, 7353fb920de39d5a7157248ccd7bed40bcb0bebdd845c178394b3990c62b16e4, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4194 +3b3811ad2d7cae19dc1ae072995144777b38834df1c7afeb42acba448e89bc94, c79c8599b14a928e590bc08267b46687b669e44f8c2f17ec5cd546757898b23f, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4279 +b24eb5baaeaed3c61f518ca9977e0a3ed7f096d6ded8928c9e82796db54c6db0, 6128372fb6122ce0580c8be4ccbcdf8dedda6e715e1346bdc07ce8af2fd64004, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3984 +86af826199eff97b4096cdc0a65c39d3a7999943d5c972b986a4790de3c6834e, ef1d45ca4e7798874551587794b46c2f8a326a65ef2606a816edf640a4e64f35, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4198 +95f4c1fe80bd070241363c54a7cacb2700f676d27ef3be667ea4c4468972faec, 1d7f731c233ce24a8ef8c8554e632e559b86a36077e9ef383aa35555a99ba93a, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4112 +2fa2b7502405b29c91d860624137128d014805c731c995f1e8ea0618375bef71, 487c98844fd5e508b91bb51fe106c29053b2d789ce34f83eb25b9236d044ce54, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4445 +43a22b7e74dbdcb06eaf05af45e973ecd0516618c0cdc5c5f862eba3930ec6ca, d45e68a4f7bc514ddadddd88563cb1149d1a4627176cbc889c646bd993f7d9dc, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4525 +c78d83d544cde30940a7c2e79dacd89b7be67bfc2f675246b0c900c3348be47b, 7f07b4b55f5461eaca22747939121bcdd480eaa3e52fe6eaae533e87b5bab6f0, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4079 +0868b5d914ab9fd4286e8c1288d0ad20279cfecc4fb62f7a0261349593868034, 2b18adeef5443c713cdd1eb669bd522f811e148b51becee2ec7fa076121fef3d, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4068 +8c67119e8866f54e299659373bfcdb50658f132f5838f5f98f2a6cccfa694582, 6f1983a520ad271eb4b3a0feb3d78379df1aa1ca6b1d39270f0800b052220206, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4035 +567a12bb956b64450bdc1873cb916ffb2e5aa647b44a6e7c694b96ac4939f80f, 3f4be93dd2bd342345973a164c36991a2eaa123ab679e2b662cf79f3fadaa1f3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3967 +cef39307dc5f8b905ffaa40b74dd7a6ae28e9b20ad410aec447fc1e3faeb3109, 6d898904e6a64658c756f2bd12ca465fe79a8e177bcc7c08ad72acb543d455d3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3995 +b5f70c7915c32b6304048d682a5f56a27a6e6647e03ea626a9ce66a1e5b74882, a07872db7e1665817caa07bfc7c05fcf8a316ce17601aa64a280095ee4a47f3f, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3994 +05bd14c740eccef297fe0c36dbce6dfcf3a573c5c557dd1762d8a91df58db2ad, f181307bc0c696c266e89d214846dcef1f2ae46fa6ab0c755f7021bafcec2458, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4241 +3c635171bf5e2c20d0464af2752f665927946f7da667f26a6169212d0c7b9e19, 71b317f8d47857d19c26ce96f47c246f24bc359bc94aec5fae3617815eaa0627, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3924 +4cf7eced94f73f92f1051c49993c02464a46ed9b4cf992b8a1b32e82c8e7ee53, 81249a9488535d742ac2f0604a56d0e03731fdf883909f81a67140ee2543a7ae, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4830 +b21455f84dd299a4e7eecf59bc16a4dac3693bed9d7dc39205a790f387218891, 091300da9360265d7f95e331d1aa7cc5bd34d20ad29d644c7187614c1c96855e, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4048 +7a916acd1bf9c4ce52457eadb627bc8de362fd77f7ce0362e810dbed21547378, 2050a738dd2fb5a268cd781a4881dddb52066b7d75817bd06f296ef5be6fa856, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4104 +272470c8ca87d42d07870a914c9e980b32104b32de7b9a51b8c7c984f79c2c69, 56f4c134b9cb6dec98c81a8401fb1971cf78211d2ad4f79f3d7dfc8d9b0301a5, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4483 +1a3f8d93141ad5e3ac1a90e9f46d1f32631bf694bed474c7962d651e1fd26881, 4add95fc66f5aa5d9938c8e32dddc7c70ad054f114507cb95e14ae42bb253d96, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3987 +76e7e980a4199b13a0417db3d722a4558c586885bce7e6679467835f6fc253ff, 5e524a4a4c0e39e44a83e2040a2ffc4b536098e1a4659cef228bf326d7130b54, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4072 +e20a1940ddaa1371af7e7ea919f8064fbe50df43830bab732931aa1900b7acbf, 82be747d83026cc2bb90f1f551a5abd16ac9d635c4723004751c26ac15549a1b, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4012 +59b8ff49ba21f829bcb682a9e51acd7c4bd43726f8e8131e7c0399dc15d1e3e0, 18a3e10ca629fa686ba5f4cbd5440dddfe82f874175f6d77b822012a1cf54b0a, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4250 +87a835df7f5e7703fc019bedb0e94a7ff63a715ebdce83d5e0fb178119e4f06e, cecfc2663701707933ef70ad09a70de9c4cb49c324e5a666bf2941b481065ac0, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4029 +ff23e4cea9eef8e9ccbdb7febbe8a696c77398b42ee05b3eb7fce8ffba9f22d0, 647d7cd0fc289c2f923ab330f2fd39d476c284e41bdd98e9e1d517e3014e0511, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3958 +de7688270f4f7f936bb26eea7e35340b5de7ae9ef76ec13e160864dd133d88ba, 229d383c968182630f5216ae7621c8e8d509a2facb150dc0853b14c88ead9698, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3873 +5d6908a6dab0c1a5842f8fbbef4b9056aa937c8391b95b2d821241bad4640ce3, 35c4480634c28602acffa168e4a46f402ae7e006e1a6226d4a17d0abcf377a1a, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3968 +8d47429fff684d4ecfb63e5a71bce0fbc73812897d2306c267065354e14d63bc, 2e05ceae39061864f93d20a95c48a778de47ea4505976e8aa5a4d53eff95f389, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4294967295 +86c34012e9381c9983251165bf62206dfccda9d51e05349b852424b51a5f5791, 60979a1a72c4557f8238411fd06230ad600ad5bc5cd0cd528d27e04d048bb3f9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4327 +73ac3e8e5d99461848fe285df80ff1207c0c1fe9e474120e44a7da1d6cc680af, e076dc58b469918cc5edb8813eff740951bad4f52e5a532b69271e62dd4604c2, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4171 +2df1baf2e452551a257fa2171e7e9e1f60c729b088dd70293c0e3ce9d1259fed, d3b9433e36f40ae4663ac9cac9e6086129b4b9a6d4c713cd431f3dd4574157aa, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4071 +950a839b99846d1a7a9d3abaa180636d785915bd85fbdad3565dab051f7dd889, dcb4544c304036f9c50a23a2a2677e29f4d401455f5ee714ce70ae2c40d964b8, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4104 +633fbdaa31e766e271591f8e6cf2a5e39087166cd48fdebdc3fd44f57c07fd04, 4f37c954c7a00bff07f402eca1fe37062f3b33342b8891c4d4564d0588239a84, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4013 +dba0248958481896f41ffe6d255636f65f311dfa518fb6ff7fb1dde7d7ba083c, fa909b9c1d0a7f4ef21142ee1b10a6fba3562dc83676c4e34862165650480ab0, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4031 +51493015b513ab69f8f44def0202a6783a6a33ba19c4a43ddd393696e6c6de09, d7946e834695f2e0ef98b7a34b720f70deab58add43e678634c2e6ac6b225f6a, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3952 +7e0923ab0d299c31ffe97ce203e48e816fbce737e7603057bc14698c50e454cb, 7e79180baf44db64e4e65f512b335bc16d8e214709d913455334962812e66ea5, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4119 +836253c93cfc7660d008e3ca8f5dbb4fb970daf0c91ed4fc99468d44e481cf7e, 9e3776397e6b4d06500f5d5f985bf3d6225d884677d0b802b906b8f37a10cea0, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4128 +204a5494b217b0fa69ed9c8580c28ea64055fc8c7f22082f574dc271f8ef83d4, f24f9d48d1ef5dd8b386d10e77010b2c73555e2a8020e82ff6c2680e4637bcd2, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4134 +ddc72b919c2359cd39b7939aa65a4ecd97fd399d66f2a1b78fbcab52c4ead007, 4020b0d9a331befeaf3964ac11435baa56827bf3c82bce6de0c0e6facad7f465, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3948 +49c4411d211cf2e58ef0eee11727922ca997ab48e1fced6afc73e67901f7fe4b, b4f5fc06c493a28645f74f49ad5931c4facef97821e752e27404a0f5a515c808, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4063 +1e9416b24af8a319b2559c37d46b3e8e2dd9fcdd688d9b0c9d87f5bbaedf988c, 6671e7215aa9b2e8bdafdd6c956e6c079469537c2714782992b660cbacb12aa6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4294967295 +83e18b0f2174692da8efc778104fd8ad559a8877def7a78d19fc3e7703b6f908, 17dab68f4b8027af7edb6dbeb8ee92225398763cae2a06176034c26305b2d50c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3961 +42a88e5f44f823ce807cc95980312f9a9064a0734064712eedb21d84227f5f8d, 6161140438c6001c2997a4614b4c02e71f45721eea5a0380b9b5a0f0a9369b67, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3926 +4f54d8fdfa317ee4a0132781e6923c850774fbdeffedb5a7cc32b4b3bba723a8, cbdfe8ac2cfd9ec657fbafe379a6fd31b8e62e4b39d8b5fcd325a6c01cf2c212, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3981 +d2dfcd4e82677b03cf1ca02f6ff518cff7b991a9ad139bf10dc3d4dc0a49a720, da69a84301141ca55e791ef6596eb8379f76db494d7186f64f1cd869ee537135, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3975 +2287e5cc8f335e611a972a4b5a2f88dc0973fafde8e43b0ad63418e8f12e7f6c, d9b558052a2435163012591e988cd0838d2e3aa7244c26a1b147c8a3d2998156, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3960 +bee65d4b28d44ea33146dea8b373eec6d5e2d8827d3ea92fff17425e05feec25, 75bc42813109f42075d132dd809867203a728477c80eb20c15a2842da431beee, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3946 +a3b1f532d43d656d7bed4b9d2ff8a44190fe3642d0228bf81a1aaef6c95fd2e2, 305b210d72143091c54d0bf96584aa928c61a75bba3e94d905b860bbeb509190, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4172 +54908b3b71a0f46a23125e35c2852497e42afd57413c8098eb286046676ea102, 261465d24c4e55ca775b4aa5626124de3dd222641d5baf8d95635b1683cf3212, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3994 +f9bb99de949e8f9c70fa8ae4ea0533fe4fd02c8172c97aa47c65f0a86a1a9733, 76550a5bf77ca1bccf693d7a36688a5e753eb459618dd4f80d5389e579204baf, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4038 +347963dadbe084b61bb4001be819e02668519ccd27b15d7743685e391ec30e28, 51a478d941a5629d3459e498d8cb6b0b95ac5bf3822aee799d40755ee1033e81, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4082 +37300bb5fbf3521a7247943ff8bed828c2db93fa837001eb0625883f2b30846e, 2eef924fc10970e83f06fef44bf919a9179f4dce6332b366e64993d6e8b6dc99, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4076 +2c84e6be8d6e60989311f0de9c2176b7172dec9b41717e5d8cdbce17b359e586, 987bcae10727dd07db008123363dcf05b648e3d3be585dbb750c4b4ce7b04a17, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4022 +a044257dba17f3f818ea7867dc8d2210272fdf9a6c49d7293dd2d6c8f74c38e0, e58f5d95d7634aa49c36a3af0bb639dfff50d56cc009fc6ad6966680d8b38432, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4058 +b863c20a85a4f855d09d6459fbbd44ee2675beb82589b88d891119e6ffaf9a22, 846f5192c83d0b83b04946cd656e7d22f5fc9279e738ed67802a1526fc9ada75, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4051 +8842676e4ef79dd65c96c8838490e0f71c5e0587fd472392f1c2568aa4b16d0e, 2350214ac3af8c12542a3f59c2c9ef563dfad32dc129880cb21314cac35801f6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4002 +88ca5952066524cd9af9c4d452c0d40adedebdcd15be2cca4cfa7c25d8fed68e, 827a81e2f47c4b9d06e88a9c6c4b378505f5da182130e931976f786d9327f2d3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4022 +e1bc3740297568ba64ce7e65c78e8e3910ec87bbcafb461257d4ba92da25e00f, 94dfb94f48cf22ba93783bd3fe767717a0fecf0be4b6f407764f24a99c906a72, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4054 +5c31de2265febcfa88b17f7cc4ac351bd0d55d1e27f8bb3114cdfb73929dd793, f779b1c36dc62d90f2d134c5f024c2fc9458dad86d2437c4b2adcae4c26ab8e9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4096 +6d36632caef4b8005dd52f0cb2478c73782a8b769861c6a481617477c845c9c0, 2d4a99d7e6cc7e8b6dbdada85c673168c9ca18933e900860da05b45d84b45f48, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4011 +9765fb5c321cbb53694351e3d6b608ff4e91bccf85867e6f36b8a9b588d99daf, c9c12209c36e8ee50c4cb6271aa34deb8b19fe5947be9775fdca32d1ed11e3f4, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3982 +70ba464f9f4ae5cdbf84645ded1860f9040eb73d8f4217bc37fd5de96efc0e1f, a6b9fcdb4d4ce11c947b23dce9a72bd57cf5e39dd929b49c4353942014ceb689, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4030 +f35ab2abc229b23a84e747b56ba655aa80ec43f8123639de199b339ce0988afd, 763f7bb911922d08c3b21d6346b02ed124319262329774a1e1e9108ed9f36e3c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4038 +ba3929fa4120a0e76bfb93a3d345169ead7d551ffbc067217b75d0da3d3239e9, c216ed9e232d0fa509a82bba3aa1fb910c77e2829ba403e2adb4388d4d778ef9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3945 +0c94637c6ef6411516af889a492757b2125031c975fe6ac0e311e0354cf5343d, a7bc7631babeddd33aacac22fccd6eb8f97e1b6280fd5209d07df7ced41b20fa, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4217 +b6eaae827e70dc56049a0cf6b9714eda2a254f23de838a015b7f96b1aab466d4, a2dd155fdcf3480ad4e49e782d3ac61c152191c83bfb19f7e00a9951fe6e16f6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4554 +c091ca2227954b5334e5485a8a0a4fb3b1cc9dcd3fe3d5b5cbdf5764962766e8, 10b88367348be5e3ff5f070f0cdb035f3edd0c571a18dd45b43bb41f43554ca4, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4419 +773bc69bbc4d0aba482e0c3f9da033d2099694ab1470cf01712db4f4c9c68041, bf02268b41216a96fe90692d60adb3cc3b35516f533e991530495f50ffdf9926, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4436 +2c648cb8f34052477c5b5c668a42ce9ec1d6f93b0a13fdac21a85ad391fb6b4a, 04083fc4261e3b3b6b5df0562394724fe906704d65ac8d8fdbb2231ede1eafcb, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4073 +8039cd218bb0968fc8408bc001cbb5a6fcb5816a0bc661830fc904bde80a1ae7, c59fd8b643fdbf435dd1f114467c9029dccfac89220f66c11d0bab9ac4a14792, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3924 +d722442174e215ccdba594ac37c83abe81e07590ccc77b6dac24836e9f4ddfe4, ddd764aa3f1e1711ca8ffb23228d29a85a1d7b9c9202b08ee182a33c9c0f5246, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4050 +17d5a1fef4b69dc6a1aa176383a2a64a71a68036c20726b031034058ad430321, 36688dd1635866dc842651320845bf1b25ba17bb57ffe403e20c1342c3c08491, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4060 diff --git a/test/score.cpp b/test/score.cpp index 2fe37b43..aa8cdace 100644 --- a/test/score.cpp +++ b/test/score.cpp @@ -546,7 +546,7 @@ TEST(TestQubicScoreFunction, Bpp9000AntColonyRegression) const unsigned char* pool = pools[chain.poolIndex].data(); score_engine::ScoreBpp9000::ANN parent; - engine->deriveRootANN(chain.pubkey.m256i_u8, pool, parent); // depth 0's parent = the derived root + engine->deriveRootANN(uniqueSeeds[chain.poolIndex].m256i_u8, pool, parent); // depth 0's parent = the shared epoch root for (size_t d = 0; d < chain.nodes.size(); ++d) { const AntNode& node = chain.nodes[d]; @@ -874,42 +874,53 @@ TEST(TestQubicScoreAntColony, AnnSurvivesExpandAndCompact) EXPECT_EQ(memcmp(&restored, &original, sizeof(original)), 0) << "expand/compact is not lossless"; } -TEST(TestQubicScoreAntColony, RootAnnIsDeterministicAndPerIdentity) +// The root is shared per epoch: it is a function of the root seed (the epoch-start spectrum digest) +// alone, so the same seed must give the identical root every time, and a different seed (a different +// epoch) must give a different root. +TEST(TestQubicScoreAntColony, RootAnnIsDeterministicAndShared) { AntFixture f; ASSERT_TRUE(makeAntFixture(f)); - const m256i pkA = makePubkey(4); - const m256i pkB = makePubkey(5); + const m256i seedA = makePubkey(4); + const m256i seedB = makePubkey(5); AntEngine::ANN a1; AntEngine::ANN a2; AntEngine::ANN b1; - f.engine->deriveRootANN(pkA.m256i_u8, f.pool.data(), a1); - // Deriving another identity's root overwrites initValue.lutInit, which is the state a1 came from. - f.engine->deriveRootANN(pkB.m256i_u8, f.pool.data(), b1); - f.engine->deriveRootANN(pkA.m256i_u8, f.pool.data(), a2); + f.engine->deriveRootANN(seedA.m256i_u8, f.pool.data(), a1); + // Deriving another epoch's root overwrites initValue.lutInit, which is the state a1 came from. + f.engine->deriveRootANN(seedB.m256i_u8, f.pool.data(), b1); + f.engine->deriveRootANN(seedA.m256i_u8, f.pool.data(), a2); EXPECT_EQ(memcmp(&a1, &a2, sizeof(a1)), 0) << "root depends on engine state"; - EXPECT_NE(memcmp(&a1, &b1, sizeof(a1)), 0) << "two identities share a root"; + EXPECT_NE(memcmp(&a1, &b1, sizeof(a1)), 0) << "two different seeds share a root"; } // A child is a function of (parent, pubkey, nonce, anchor). Same inputs must give the same score AND -// the same inherited LUT; a different parent must not give the same child. +// the same inherited LUT; a different parent must not give the same child. The root is shared, so +// the second parent is a mutated child of the root rather than another identity's root. TEST(TestQubicScoreAntColony, ChildIsDeterministicAndInheritsParent) { AntFixture f; ASSERT_TRUE(makeAntFixture(f)); + const m256i seed = makePubkey(4); const m256i pk = makePubkey(6); const m256i nonce = makeAntNonce(5, 3, 41); const m256i anchor = makePubkey(12); AntEngine::ANN parentA; + f.engine->deriveRootANN(seed.m256i_u8, f.pool.data(), parentA); + + m256i improvingNonce; + ASSERT_TRUE(findImprovingNonce(*f.engine, parentA, pk, anchor, f.pool.data(), improvingNonce)) + << "no nonce improved on the root, so no distinct second parent can be built"; + f.engine->computeScoreFromParent(parentA, pk.m256i_u8, improvingNonce.m256i_u8, anchor.m256i_u8, f.pool.data()); AntEngine::ANN parentB; - f.engine->deriveRootANN(pk.m256i_u8, f.pool.data(), parentA); - f.engine->deriveRootANN(makePubkey(7).m256i_u8, f.pool.data(), parentB); + f.engine->getBestANN(parentB); + ASSERT_NE(memcmp(&parentA, &parentB, sizeof(parentA)), 0) << "the mutated child equals the root"; const unsigned int s1 = f.engine->computeScoreFromParent( parentA, pk.m256i_u8, nonce.m256i_u8, anchor.m256i_u8, f.pool.data()); @@ -966,18 +977,17 @@ TEST(TestQubicScoreAntColony, NonCanonicalNonceIsRejected) AntFixture f; ASSERT_TRUE(makeAntFixture(f)); + const m256i seed = makePubkey(4); const m256i pk = makePubkey(10); const m256i anchor = makePubkey(30); - AntEngine::ANN parentA; - AntEngine::ANN parentB; - f.engine->deriveRootANN(pk.m256i_u8, f.pool.data(), parentA); - f.engine->deriveRootANN(makePubkey(11).m256i_u8, f.pool.data(), parentB); + AntEngine::ANN parent; + f.engine->deriveRootANN(seed.m256i_u8, f.pool.data(), parent); constexpr unsigned char maxK = (unsigned char)AntCfg::numberOfMutations; const m256i good = makeAntNonce(3, 5, 62); // A rejected nonce and a timed-out walk - f.engine->computeScoreFromParent(parentA, pk.m256i_u8, good.m256i_u8, anchor.m256i_u8, f.pool.data()); + f.engine->computeScoreFromParent(parent, pk.m256i_u8, good.m256i_u8, anchor.m256i_u8, f.pool.data()); AntEngine::ANN afterA; f.engine->getBestANN(afterA); @@ -993,7 +1003,7 @@ TEST(TestQubicScoreAntColony, NonCanonicalNonceIsRejected) for (unsigned int i = 0; i < numberOfBadNonces; i++) { // The bad nonce is early rejected in computeScoreFromParent() - EXPECT_EQ(f.engine->computeScoreFromParent(parentB, pk.m256i_u8, bad[i].m256i_u8, anchor.m256i_u8, f.pool.data()), + EXPECT_EQ(f.engine->computeScoreFromParent(parent, pk.m256i_u8, bad[i].m256i_u8, anchor.m256i_u8, f.pool.data()), score_engine::INVALID_SCORE_VALUE) << "non-canonical nonce " << i << " accepted"; AntEngine::ANN now; @@ -1001,8 +1011,11 @@ TEST(TestQubicScoreAntColony, NonCanonicalNonceIsRejected) EXPECT_EQ(memcmp(&now, &afterA, sizeof(now)), 0) << "rejected nonce " << i << " still ran the walk"; } - // Now after bad nonce, we feed good nonce, we expect this is ok - f.engine->computeScoreFromParent(parentB, pk.m256i_u8, good.m256i_u8, anchor.m256i_u8, f.pool.data()); + // Now after bad nonces, an improving canonical nonce on the same parent must move bestANN again + m256i good2; + ASSERT_TRUE(findImprovingNonce(*f.engine, parent, pk, anchor, f.pool.data(), good2)) + << "no nonce improved on the parent, so the post-rejection walk check would be vacuous"; + f.engine->computeScoreFromParent(parent, pk.m256i_u8, good2.m256i_u8, anchor.m256i_u8, f.pool.data()); AntEngine::ANN afterB; f.engine->getBestANN(afterB); EXPECT_NE(memcmp(&afterB, &afterA, sizeof(afterB)), 0) << "canonical nonce was not scored"; From 1d9fb4d6124ea3e1c21fe5eba7abf19dc3299bd3 Mon Sep 17 00:00:00 2001 From: N-010 Date: Tue, 1 Sep 2026 13:02:56 +0300 Subject: [PATCH 10/21] New version of Nostromo (#842) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor Nostromo contract: replace tier-based features with auction functionality, including participant management, lot handling, and visibility settings. * Refactor `CreateAuction` procedure in `Nostromo` contract: modularize auction parameter resolution, lot asset management, and data validation into reusable private functions. * Refactor `Nostromo` contract: remove obsolete structs and functions, simplify auction initialization, modularize parameter resolution, and enforce duration cap validation. * Add `ValidateMetadataCid` procedure to `Nostromo` contract: introduce IPFS CID validation with character checks and integration into `CreateAuction` function. * Add buy-now and price validation to auction parameter resolution in `Nostromo`: enhance `resolveBatchAuctionCreateParams` and `resolveStandardAuctionCreateParams` with price-related inputs and validation logic. * Add sale price validation in `ResolveBatchAuctionCloseParams` of `Nostromo`: ensure `salePricePerUnit` is non-zero and less than or equal to `initialPricePerUnit`. * Refactor `Nostromo` contract: remove unused auction fields, improve bidder validation, and refine access asset checks for private auctions. * Add auction cancellation functionality to `Nostromo`: introduce `CancelAuction` procedure with fee logic, participant refunds, and state updates; refactor auction fields to use total prices instead of per-unit pricing. * Expand access asset logic in `Nostromo`: replace single asset with multiple required assets, add validation functions, and refactor private auction checks. * Refactor private auction access logic in `Nostromo`: replace boolean flags with `uint8`, enhance access validation with `hasAccess`, and refine checks for required access assets and allowed wallets. * Modularize bid processing in `Nostromo`: introduce `ProcessBatchBid` and `ProcessStandardBid` procedures, refactor `PlaceBid` for auction-type-specific bid handling, and encapsulate participant and auction updates. * Add `@brief` documentation across `Nostromo` contract: enhance struct clarity with detailed field descriptions, standardize comment format, and improve internal documentation consistency. * Simplify escrow logic in `Nostromo`: unify per-asset and lot-based pricing in auctions, remove redundant bidder and escrow fields, refine bid processing procedures, and enhance refund and validation logic. * Update comments in `Nostromo` and add participant cleanup in auction rollback: refine `@brief` documentation for price fields, standardize comment formatting, and invoke `participants.cleanupIfNeeded()` during rollback. * Add storage capacity check in `Nostromo` auction participation: prevent new participants if storage is full and return `StorageFull` error code. * Add auction pause handling and tick-based scheduling in `Nostromo`: introduce post-epoch auction pause mechanism, tick-based scheduling for auction finalization, and new procedures for batch and standard auction finalization. * Refine `minimumPurchaseQuantity` handling in `Nostromo`: update comments for clarity, remove unused checks in input validation, and simplify auction setup logic. * Add revenue distribution and seller decision handling in `Nostromo`: implement `DistributeAuctionRevenue` for fee allocation, introduce `ResolvePendingStandardAuction` for seller decisions, and refine auction finalization procedures to support revenue sharing logic. * Expand auction handling in `Nostromo`: add structured input/output for auction procedures, refine field documentation, and introduce detailed `@brief` comments for clarity and consistency. * Refactor auction type handling in `Nostromo`: replace conditional logic with `switch` statement and improve inline documentation for batch auction finalization procedures. * Add highest bid recomputation for batch auctions in `Nostromo`: implement `RecomputeBatchHighestBid` procedure, update bid processing to trigger recomputation when needed, and introduce supporting data structures and logic. * Add dynamic auction fee configuration in `Nostromo`: introduce default fee constants, enable runtime fee updates via new procedures `SetAuctionFees`, ` * Add `GetAuctionFees` and `GetFeeRecipients` procedures in `Nostromo`: expose current fee configuration and recipient wallets via structured input/output, register new user functions. * Refactor and streamline `ContractTestingNostromo`: remove redundant testing logic, consolidate helper functions, and introduce `ContractTestingNostromoAuctionFromScratch` class for auction-related test cases. * Add global auction pause mechanism in `Nostromo`: implement auction timer pause intervals, synchronize deadlines with pause states, and add test cases for deadline adjustments. * Refactor `ContractTestingNostromoAuctionFromScratch` to `ContractTestingNOST`: simplify test class structure, introduce `NOSTChecker` for state interactions, and streamline auction-related helper functions. * Synchronize auction pause states with `SyncAuctionPauseState`: ensure accurate timer adjustments, align deadlines with current pause state, and refactor balance-checking logic in tests to improve test accuracy. * Add `expectedDividendPoolIncrease` helper in tests: calculate expected shareholder dividend pool changes and replace direct comparisons with computed expectations. Update relevant assertions for accuracy. * Refactor `GetAuction` output structure: replace `AuctionData` with `Array` to support consistent handling of auction data, update all related method calls and tests. * Simplify `GetAuction` output: replace `Array` with `AuctionData`, update method calls and tests accordingly. * Introduce auction service fee breakdown: add calculation, distribution logic, and integrate with private auction and cancellation flows. Update related tests and constants. * Refactor `TransferShareManagementRights`: introduce locals structure, implement safer flow with validation checks, and ensure proper invocation reward handling and refund logic. * Refactor auction data structures: introduce `AuctionCore` for shared fields, separate persistent state (`AuctionData`) and ABI views (`AuctionView`), and update all references and logic to use the new structure. * Refactor `ContractNostromoAuction` tests: replace nested auction fields with `AuctionCore` references, update related assertions, and introduce `seedUser` to streamline user initialization logic. * Introduce extended auction handling methods and storage validation: add `placeBidWithFundedReward`, extend `transferManagedShares` with reward options, integrate private auction access lists, and ensure proper handling for scenarios like storage full conditions. Update tests accordingly. * Reduce auction configuration limits: decrease lot items, allowed wallets, and required access assets for improved efficiency. * Replace `auctionId` with `auctionIndex` across `ContractNostromoAuction` methods for consistency and clarity. Introduce expanded auction getter methods. Update related tests and assertions. * Fix contract spacing, formatting, and minor adjustments in `ContractNostromoAuction` tests for clarity and consistency. * Update `allowedBidder` values in `ContractNostromoAuction` tests to reflect revised wallet IDs. * Add error code handling to `transferManagedShares` and auction methods. Update tests and logging to validate outputs and ensure consistent error propagation. * Changes to the order of fields in NostromoProcedureLog * Changes the field type from EAuctionError to uint32 in NostromoProcedureLog * The wallet whitelist has been expanded, each lot is limited to a single asset, and asset-based access now takes into account the minimum required quantity and validates the input data. * Removes magic numbers * Batch 1 lot, Standard 4 lots per auction * Adds comments * Minimum Purchase Quantity in Batch Auction * Add minimum price and bid increment for Standard Auctions; implement batch auction bid availability check Change hashmap to array * Add batch auction creation fee and emergency pause configuration; implement fee reserve guard logic * Refactor batch auction bid fee calculation; remove unused tokensForSale parameter and adjust fee logic * Add detailed documentation for auction-related functions; enhance code clarity * Refactor auction creation fee handling; unify public auction fee constants and update related logic * Update contract state management and introduce old state data structures * • feat(nostromo): harden auction lifecycle and payout handling - retain full closed-auction snapshots and participant history - release active storage after auction finalization or cancellation - queue failed QU payouts and retry them automatically at END_EPOCH - add payout capacity guards and replace magic numbers with constexpr values - optimize retained-auction getters and batch availability lookup - extend contract statistics, error handling, documentation, and tests * update private auction access logic to allow combined access modes * implement shared fee pool management and retrieval functions * remove NOSTChecker class and streamline auction fee calculations * Moved comments for verification processing * Update NOST_CONTRACT_INDEX migration version to 228 --- src/contract_core/contract_def.h | 2 +- src/contracts/Nostromo.h | 7300 +++++++++++++++++++++++++----- src/qpi/impl/qpi_system_impl.h | 33 +- src/qpi/qpi_context.h | 3 + test/contract_nostromo.cpp | 5647 ++++++++++++++++------- 5 files changed, 10090 insertions(+), 2895 deletions(-) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 2aa6e074..c8d81ba6 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -580,7 +580,7 @@ struct ContractStateChangeInfo // When enabling, replace both lines below, e.g.: //constexpr ContractStateChangeInfo contractStateChangeInfos[] = { { DUMMY_CONTRACT_INDEX, MIGRATE, 219 } }; //constexpr unsigned int contractStateChangeCount = sizeof(contractStateChangeInfos) / sizeof(contractStateChangeInfos[0]); -constexpr ContractStateChangeInfo contractStateChangeInfos[] = { { QIP_CONTRACT_INDEX, RESET, 224 }, { RANDOM_CONTRACT_INDEX, PADDING, 224 } }; +constexpr ContractStateChangeInfo contractStateChangeInfos[] = { { QIP_CONTRACT_INDEX, RESET, 224 }, { RANDOM_CONTRACT_INDEX, PADDING, 224 }, {NOST_CONTRACT_INDEX, MIGRATE, 229}}; constexpr unsigned int contractStateChangeCount = sizeof(contractStateChangeInfos) / sizeof(contractStateChangeInfos[0]); diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 1e50441e..e3e34c84 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -1,29 +1,146 @@ using namespace QPI; -constexpr uint64 NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT = 20000000ULL; -constexpr uint64 NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT = 100000000ULL; -constexpr uint64 NOSTROMO_TIER_DOG_STAKE_AMOUNT = 200000000ULL; -constexpr uint64 NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT = 800000000ULL; -constexpr uint64 NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT = 3200000000ULL; -constexpr uint64 NOSTROMO_QX_TOKEN_ISSUANCE_FEE = 1000000000ULL; - -constexpr uint32 NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT = 55; -constexpr uint32 NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT = 300; -constexpr uint32 NOSTROMO_TIER_DOG_POOL_WEIGHT = 750; -constexpr uint32 NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT = 3050; -constexpr uint32 NOSTROMO_TIER_WARRIOR_POOL_WEIGHT = 13750; - -constexpr uint32 NOSTROMO_TIER_FACEHUGGER_UNSTAKE_FEE = 5; -constexpr uint32 NOSTROMO_TIER_CHESTBURST_UNSTAKE_FEE = 4; -constexpr uint32 NOSTROMO_TIER_DOG_UNSTAKE_FEE = 3; -constexpr uint32 NOSTROMO_TIER_XENOMORPH_UNSTAKE_FEE = 2; -constexpr uint32 NOSTROMO_TIER_WARRIOR_UNSTAKE_FEE = 1; -constexpr uint32 NOSTROMO_CREATE_PROJECT_FEE = 100000000; - -constexpr uint32 NOSTROMO_MAX_USER = 262144; -constexpr uint32 NOSTROMO_MAX_NUMBER_PROJECT = 262144; -constexpr uint32 NOSTROMO_MAX_NUMBER_TOKEN = 262144; -constexpr uint32 NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST = 128; +namespace QPI +{ + inline bool operator==(const Asset& lhs, const Asset& rhs) + { + return lhs.assetName == rhs.assetName && lhs.issuer == rhs.issuer; + } +} // namespace QPI + +// Maximum number of active auction records stored by the contract, in auctions. +constexpr uint64 NOST_AUCTION_NUM = 2048; +// Number of full closed-auction snapshots retained in the history ring buffer, in entries. +constexpr uint64 NOST_AUCTION_HISTORY_NUM = 1024; +// Fixed length of an auction metadata IPFS CID, in bytes. +constexpr uint64 NOST_AUCTION_METADATA_CID_LENGTH = 64; +// Maximum number of active auction-participant bid records, in entries. +constexpr uint64 NOST_AUCTION_PARTICIPANT_NUM = 4096; +// Maximum number of wallets with unpaid QU obligations retained by the contract. +constexpr uint64 NOST_PENDING_PAYOUT_NUM = 8192; +// Maximum pending-payout slots one auction settlement may require before END_EPOCH fee distribution. +constexpr uint64 NOST_AUCTION_REVENUE_MAX_PAYOUT_RECIPIENTS = 1; +// Additional pending-payout slot reserved for the Batch bid caller's possible overpayment refund. +constexpr uint64 NOST_BATCH_BID_CALLER_PAYOUT_RECIPIENTS = 1; +// Maximum pending-payout slots reserved by a Standard bid for refunds and an immediate Buy Now settlement. +constexpr uint64 NOST_STANDARD_BID_MAX_PAYOUT_RECIPIENTS = 6; +// Maximum pending-payout slots reserved by Standard settlement for revenue distribution and bidder handling. +constexpr uint64 NOST_STANDARD_FINALIZATION_MAX_PAYOUT_RECIPIENTS = 5; +// Number of QPI-sized QU transfer chunks attempted for an immediate refund or settlement payout. +constexpr uint64 NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL = 1; +// Maximum number of pending-payout wallets retried automatically during one END_EPOCH call. +constexpr uint64 NOST_END_EPOCH_PAYOUT_RECIPIENT_NUM = 64; +// Number of QPI-sized QU transfer chunks retried per pending-payout wallet at END_EPOCH. +constexpr uint64 NOST_END_EPOCH_PAYOUT_CHUNKS_PER_RECIPIENT = 1; +// Maximum number of QPI-sized QU transfers attempted for one wallet in one procedure call. +constexpr uint64 NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL = 16; +// Sentinel for "no participant slot". +constexpr uint64 NOST_INVALID_PARTICIPANT_SLOT = NOST_AUCTION_PARTICIPANT_NUM; +// Maximum number of entries returned by one paginated auction getter call. +constexpr uint64 NOST_AUCTION_GETTER_PAGE_SIZE = 64; +// Maximum number of asset entries in a Batch Auction lot. +constexpr uint64 NOST_BATCH_AUCTION_LOT_ITEM_NUM = 1; +// Integer offset that makes the Batch coverage threshold include the first quantity below the minimum allocation. +constexpr uint64 NOST_BATCH_COVERAGE_THRESHOLD_OFFSET = 1; +// Maximum number of asset entries in a Standard Auction lot. +constexpr uint64 NOST_AUCTION_LOT_ITEM_NUM = 4; +// Maximum number of bidder wallets allowed by a private auction wallet gate. +constexpr uint64 NOST_AUCTION_ALLOWED_WALLET_NUM = 16; +// Maximum number of alternative assets accepted by a private auction asset gate. +constexpr uint64 NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM = 4; +// Maximum configured duration of any auction, in days. +constexpr uint32 NOST_AUCTION_MAX_DURATION_DAYS = 30; +// Default fee charged to create a private auction, in qu. +constexpr sint64 NOST_DEFAULT_PRIVATE_AUCTION_FEE = 50000000LL; +// Default fee accumulated after successfully creating a public auction and distributed at END_EPOCH, in qu. +constexpr sint64 NOST_PUBLIC_AUCTION_CREATION_FEE = 100LL; +// Minimum total payment target for small accepted Batch Auction bids, in qu. +constexpr uint64 NOST_BATCH_BID_FEE_CUTOFF = 100ULL; +// Default fee deducted when an auction is cancelled, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_CANCELLATION_FEE_BP = 1000ULL; +// Default management fee applied to gross auction proceeds, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_MANAGEMENT_FEE_BP = 50ULL; +// Default development fee applied to gross auction proceeds, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_DEVELOPMENT_FEE_BP = 50ULL; +// Default takeover coordinator fee applied to gross auction proceeds, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_TAKEOVER_COORDINATOR_FEE_BP = 50ULL; +// Shareholder allocation of auction creation, small-bid, and cancellation service fees, in basis points. +constexpr uint64 NOST_AUCTION_SERVICE_FEE_SHAREHOLDER_BP = 7270ULL; +// Management allocation of auction creation, small-bid, and cancellation service fees, in basis points. +constexpr uint64 NOST_AUCTION_SERVICE_FEE_MANAGEMENT_BP = 910ULL; +// Development allocation of auction creation, small-bid, and cancellation service fees, in basis points. +constexpr uint64 NOST_AUCTION_SERVICE_FEE_DEVELOPMENT_BP = 910ULL; +// Takeover coordinator allocation of auction creation, small-bid, and cancellation service fees, in basis points. +constexpr uint64 NOST_AUCTION_SERVICE_FEE_TAKEOVER_COORDINATOR_BP = 910ULL; +// Default portion of the shareholder fee distributed as dividends, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_DIVIDEND_BP = 9000ULL; +// Default shareholder fee for gross proceeds in tier 1, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_1 = 500ULL; +// Default shareholder fee for gross proceeds in tier 2, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_2 = 450ULL; +// Default shareholder fee for gross proceeds in tier 3, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_3 = 400ULL; +// Default shareholder fee for gross proceeds in tier 4, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4 = 350ULL; +// Inclusive upper gross-proceeds threshold for shareholder fee tier 1, in qu. +constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1 = 5000000000ULL; +// Inclusive upper gross-proceeds threshold for shareholder fee tier 2, in qu. +constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2 = 50000000000ULL; +// Inclusive upper gross-proceeds threshold for shareholder fee tier 3, in qu. +constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3 = 200000000000ULL; +// Time added when an accepted bid arrives near an auction deadline, in seconds. +constexpr uint64 NOST_AUCTION_EXTENSION_SECONDS = 300ULL; +// Number of seconds used to convert one auction duration day. +constexpr uint64 NOST_SECONDS_PER_DAY = 86400ULL; +// Time allowed for a Standard Auction seller to resolve a pending sale, in seconds. +constexpr uint64 NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS = 604800ULL; +// Duration of the scheduled auction pause before an epoch transition, in seconds. +constexpr uint64 NOST_AUCTION_PRE_EPOCH_PAUSE_SECONDS = 1800ULL; +// Duration of the auction launch pause after `BEGIN_EPOCH`, in ticks. +constexpr uint32 NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS = 500U; +// Denominator representing 100 percent in basis-point calculations. +constexpr uint64 NOST_BASIS_POINTS_SCALE = 10000ULL; +// Number of microseconds used to convert a timestamp duration to seconds. +constexpr uint64 NOST_MICROSECONDS_PER_SECOND = 1000000ULL; +// Epoch at which the contract reapplies its default configuration, in epochs. +constexpr uint16 NOST_REINITIALIZATION_EPOCH = 220U; +// Quantity used to sell a Standard Auction lot as one indivisible unit, not an asset count. +constexpr uint64 NOST_STANDARD_AUCTION_LOT_COUNT = 1ULL; +// Minimum allowed Standard Auction starting and sale price, in qu. +constexpr uint64 NOST_STANDARD_MIN_PRICE = 1000000ULL; +// Minimum allowed Standard Auction bid increment, in qu. +constexpr uint64 NOST_STANDARD_MIN_BID_INCREMENT = 1000ULL; +// Year component of the packed initial date stamp. +constexpr uint8 NOST_DEFAULT_INIT_YEAR = 22U; +// Month component of the packed initial date stamp. +constexpr uint8 NOST_DEFAULT_INIT_MONTH = 4U; +// Day component of the packed initial date stamp. +constexpr uint8 NOST_DEFAULT_INIT_DAY = 13U; +// Bit offset of the year component in a packed date stamp, in bits. +constexpr uint8 NOST_DATE_STAMP_YEAR_SHIFT = 9U; +// Bit offset of the month component in a packed date stamp, in bits. +constexpr uint8 NOST_DATE_STAMP_MONTH_SHIFT = 5U; +// Runtime day-of-week index on which the scheduled pre-epoch pause begins. +constexpr uint8 NOST_PRE_EPOCH_PAUSE_DAY_OF_WEEK = 0U; +// UTC hour at which the scheduled pre-epoch pause begins. +constexpr uint8 NOST_PRE_EPOCH_PAUSE_HOUR = 11U; +// Minute within the configured hour at which the scheduled pre-epoch pause begins. +constexpr uint8 NOST_PRE_EPOCH_PAUSE_MINUTE = 30U; +// Packed date stamp used to recognize the contract's initial runtime date. +constexpr uint32 NOST_DEFAULT_INIT_TIME = + NOST_DEFAULT_INIT_YEAR << NOST_DATE_STAMP_YEAR_SHIFT | NOST_DEFAULT_INIT_MONTH << NOST_DATE_STAMP_MONTH_SHIFT | NOST_DEFAULT_INIT_DAY; +// Default enabled flag that routes all collected auction fees to development. +constexpr uint8 NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT = 1; +// Default drop in the execution fee reserve that triggers an emergency pause, in basis points. +constexpr uint64 NOST_DEFAULT_FEE_RESERVE_GUARD_DROP_BP = 1000ULL; +// Default rolling window used to evaluate the execution fee reserve drop, in seconds. +constexpr uint64 NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS = 600ULL; + +/** Old */ +constexpr uint32 NOSTROMO_MAX_USER_OLD = 262144; +constexpr uint32 NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST_OLD = 128; +constexpr uint32 NOSTROMO_MAX_NUMBER_TOKEN_OLD = 262144; +constexpr uint32 NOSTROMO_MAX_NUMBER_PROJECT_OLD = 262144; struct NOST2 { @@ -31,185 +148,324 @@ struct NOST2 struct NOST : public ContractBase { -public: - /****** PORTED TIMEUTILS FROM OLD Nostromo *****/ - /** - * Compare 2 date in uint32 format - * @return -1 lesser(ealier) AB - */ - inline static sint32 dateCompare(uint32& A, uint32& B, sint32& i) + enum class EProcedureId : uint8 { - if (A == B) return 0; - if (A < B) return -1; - return 1; - } + CreateAuction = 1, + PlaceBid = 2, + CancelAuction = 3, + TransferShareManagementRights = 4, + ResolvePendingStandardAuction = 5, + SetAuctionFees = 6, + SetAuctionFeesByManagement = 7, + SetManagement = 8, + SetFeeReserveGuardConfig = 9, + SetEmergencyPause = 10 + }; - /** - * @return pack Nost datetime data from year, month, day, hour, minute, second to a uint32 - * year is counted from 24 (2024) - */ - inline static void packNostromoDate(uint32 _year, uint32 _month, uint32 _day, uint32 _hour, uint32 _minute, uint32 _second, uint32& res) + /** @brief Stable public-function identifiers used by the contract ABI. */ + enum class EFunctionId : uint16 { - res = ((_year - 24) << 26) | (_month << 22) | (_day << 17) | (_hour << 12) | (_minute << 6) | (_second); - } + GetAuctionByIndex = 1, + GetAuctionParticipant = 2, + GetTicksBeforeAuctionLaunch = 3, + GetAuctionFees = 4, + GetFeeRecipients = 5, + GetClosedAuctionHistory = 6, + GetRouteAllFeesToDevelopment = 7, + GetContractStats = 8, + GetAuctionSummaries = 9, + GetActiveAuctionIndices = 10, + GetAuctionsBySeller = 11, + GetAuctionByMetadataCid = 12, + GetAuctionSummariesByIndexBatch = 13, + GetAuctionParticipants = 14, + GetUserParticipations = 15, + GetLatestAuctionIndex = 16, + GetAuctionCountBySeller = 17, + GetAuctionAtCreationSnapshot = 18, + GetBatchAuctionBidAvailability = 19, + CalculateBatchAuctionBidFee = 20, + GetPendingServiceFeePool = 21, + GetFeeReserveGuardState = 22, + GetPendingPayout = 23, + GetNostromoFeePool = 24 + }; - inline static uint32 NostGetYear(uint32 data) - { - return ((data >> 26) + 24); - } - inline static uint32 NostGetMonth(uint32 data) - { - return ((data >> 22) & 0b1111); - } - inline static uint32 NostGetDay(uint32 data) - { - return ((data >> 17) & 0b11111); - } - inline static uint32 NostGetHour(uint32 data) - { - return ((data >> 12) & 0b11111); - } - inline static uint32 NostGetMinute(uint32 data) + enum class EAuctionType : uint8 { - return ((data >> 6) & 0b111111); - } - inline static uint32 NostGetSecond(uint32 data) + None, + Batch, + Standard + }; + + enum class EAuctionVisibility : uint8 { - return (data & 0b111111); - } - /* - * @return unpack Nost datetime from uin32 to year, month, day, hour, minute, secon - */ - inline static void unpackNostromoDate(uint8& _year, uint8& _month, uint8& _day, uint8& _hour, uint8& _minute, uint8& _second, uint32 data) + None, + Public, + Private + }; + + enum class EAuctionStatus : uint8 { - _year = NostGetYear(data); // 6 bits - _month = NostGetMonth(data); //4bits - _day = NostGetDay(data); //5bits - _hour = NostGetHour(data); //5bits - _minute = NostGetMinute(data); //6bits - _second = NostGetSecond(data); //6bits - } + None, + Active, + Finalized, + Cancelled, + PendingSellerDecision + }; - inline static void accumulatedDay(sint32 month, uint64& res) + enum class EAuctionError : uint8 { - switch (month) - { - case 1: res = 0; break; - case 2: res = 31; break; - case 3: res = 59; break; - case 4: res = 90; break; - case 5: res = 120; break; - case 6: res = 151; break; - case 7: res = 181; break; - case 8: res = 212; break; - case 9: res = 243; break; - case 10:res = 273; break; - case 11:res = 304; break; - case 12:res = 334; break; - } - } + Success, + InvalidInput, + AuctionNotFound, + AuctionClosed, + Forbidden, + InsufficientFunds, + InsufficientAssetBalance, + StorageFull, + InvalidAuctionType, + InvalidVisibility, + BidTooLow, + PrivateAuctionAccessDenied, + AuctionPaused, + AuctionIndexExhausted, + QuantityUnavailable, + AuctionHasAcceptedBid, + PayoutQueueFull + }; + /** - * @return difference in number of second, A must be smaller than or equal B to have valid value + * @brief Stores one bid slot in one auction. + * @note The same struct is shared by batch and standard auctions. */ - inline static void diffDateInSecond(uint32& A, uint32& B, sint32& i, uint64& dayA, uint64& dayB, uint64& res) + struct AuctionParticipantData { - if (dateCompare(A, B, i) >= 0) - { - res = 0; - return; - } - accumulatedDay(NostGetMonth(A), dayA); - dayA += NostGetDay(A); - accumulatedDay(NostGetMonth(B), dayB); - dayB += (NostGetYear(B) - NostGetYear(A)) * 365ULL + NostGetDay(B); + /** @brief Auction that owns this bid slot. */ + uint64 auctionIndex; - // handling leap-year: only store last 2 digits of year here, don't care about mod 100 & mod 400 case - for (i = NostGetYear(A); (uint32)(i) < NostGetYear(B); i++) - { - if (mod(i, 4) == 0) - { - dayB++; - } - } - if (mod(sint32(NostGetYear(A)), 4) == 0 && (NostGetMonth(A) > 2)) dayA++; - if (mod(sint32(NostGetYear(B)), 4) == 0 && (NostGetMonth(B) > 2)) dayB++; - res = (dayB - dayA) * 3600ULL * 24; - res += (NostGetHour(B) * 3600 + NostGetMinute(B) * 60 + NostGetSecond(B)); - res -= (NostGetHour(A) * 3600 + NostGetMinute(A) * 60 + NostGetSecond(A)); - } + /** @brief Monotonic bid sequence inside the auction, used for FIFO tie-breaks. */ + uint64 bidIndex; - inline static bool checkValidNostDateTime(uint32& A) - { - if (NostGetMonth(A) > 12) return false; - if (NostGetDay(A) > 31) return false; - if ((NostGetDay(A) == 31) && - (NostGetMonth(A) != 1) && (NostGetMonth(A) != 3) && (NostGetMonth(A) != 5) && - (NostGetMonth(A) != 7) && (NostGetMonth(A) != 8) && (NostGetMonth(A) != 10) && (NostGetMonth(A) != 12)) return false; - if ((NostGetDay(A) == 30) && (NostGetMonth(A) == 2)) return false; - if ((NostGetDay(A) == 29) && (NostGetMonth(A) == 2) && (mod(NostGetYear(A), 4u) != 0)) return false; - if (NostGetHour(A) >= 24) return false; - if (NostGetMinute(A) >= 60) return false; - if (NostGetSecond(A) >= 60) return false; - return true; - } + /** @brief Amount currently locked in escrow for the participant bid. */ + uint64 escrowedAmount; + + /** @brief Quantity requested by the participant; standard auctions always use the whole lot quantity. */ + uint64 requestedQuantity; + + /** @brief Quantity finally allocated to the participant after batch auction settlement. */ + uint64 allocatedQuantity; + + /** @brief Offered price per asset in a batch auction, or total offered price for the whole lot in a standard auction. */ + uint64 bidAmount; + + /** @brief Wallet that owns this participant record. */ + id participant; - /****** END PORTED TIMEUTILS FROM OLD Nostromo *****/ + /** @brief Timestamp of the participant's latest accepted bid. */ + DateAndTime lastBidTime; - struct investInfo + /** @brief Marks whether this fixed array slot contains a reusable historical or active record. */ + uint8 isUsed; + + /** @brief Marks bids that are still eligible for allocation or standard highest-bid settlement. */ + uint8 isActive; + + /** @brief Marks bids that remain inside the winning allocation after settlement. */ + uint8 isWinningBid; + }; + + /** + * @brief Describes an asset and quantity used by an auction lot or private access rule. + */ + struct AuctionAssetEntry { - uint64 investedAmount; - uint64 claimedAmount; - uint32 indexOfFundraising; + /** @brief Asset included in a lot or used as an access requirement. */ + Asset asset; + + /** @brief Lot quantity or minimum ownership quantity required for access. */ + sint64 quantity; }; - struct projectInfo + /** + * @brief Shared auction fields used by persistent state and public getter views. + * @note Container fields differ between persistent state and ABI views, so access-control collections stay outside this struct. + * @note `metadataIpfsCid` points to off-chain auction metadata stored in IPFS. + * @note `sellerDecisionDeadline` stays zero until a standard auction enters the manual decision window. + */ + struct AuctionCore { - id creator; - uint64 tokenName; - uint64 supplyOfToken; - uint32 startDate; - uint32 endDate; - uint32 numberOfYes; - uint32 numberOfNo; - bit isCreatedFundarasing; + /** @brief Assets and quantities offered by the auction. */ + Array auctionLotItems; + + /** @brief Lowercase base32 CIDv1 stored in Pinata for the auction name and description metadata. */ + Array metadataIpfsCid; + + /** @brief Wallet that created the auction and offers the lot for sale. */ + id seller; + + /** @brief Wallet that currently holds the highest bid. */ + id highestBidder; + + /** @brief Timestamp when the seller created the auction. */ + DateAndTime createdAt; + + /** @brief Timestamp of the most recent accepted bid. */ + DateAndTime lastBidAt; + + /** @brief Deadline for the seller to accept or reject a standard auction bid that ended between Initial Price and Sale Price. */ + DateAndTime sellerDecisionDeadline; + + /** @brief Timestamp when the auction was finalized, cancelled, or otherwise settled. */ + DateAndTime settledAt; + + /** @brief Total sale units offered; batch auctions use asset quantity, standard auctions use one unit for the whole lot. */ + uint64 quantityForSale; + + /** @brief Quantity already assigned to winning bids after settlement. */ + uint64 allocatedQuantity; + + /** @brief Minimum quantity requested by each batch bid; always zero for standard auctions. */ + uint64 minimumPurchaseQuantity; + + /** @brief Initial price for a standard auction; bids cannot start below this total price for the whole lot. */ + uint64 initialPrice; + + /** @brief Minimum selling price per asset in a batch auction, or desired minimum total selling price for the whole lot in a standard + * auction. */ + uint64 salePrice; + + /** @brief Minimum increment by which a new standard auction bid must exceed the current highest bid. */ + uint64 minimumBidIncrement; + + /** @brief Buy Now price that closes a standard auction immediately when matched or exceeded. */ + uint64 buyNowPrice; + + /** @brief Highest offered price per asset in a batch auction, or highest total offered price in a standard auction. */ + uint64 highestBidPrice; + + /** @brief Quantity requested by the current highest bid. */ + uint64 highestBidQuantity; + + /** @brief Total amount escrowed by the current highest bid; equal to the committed highest bid amount. */ + uint64 highestBidAmount; + + /** @brief Auction duration in seconds, derived from the duration configured in days. */ + uint64 auctionDurationSeconds; + + /** @brief Monotonic identifier assigned when the auction is created. */ + uint64 auctionIndex; + + /** @brief Monotonic per-auction bid index used to store every batch bid as a separate position. */ + uint64 nextBidIndex; + + /** @brief Fixed-array slot of the current standard-auction highest bid, or `NOST_INVALID_PARTICIPANT_SLOT`. */ + uint64 highestBidSlotIndex; + + /** @brief Auction House mode: Batch Auction or Standard Auction. */ + EAuctionType type; + + /** @brief Auction visibility: public or restricted private access. */ + EAuctionVisibility visibility; + + /** @brief Current lifecycle status of the auction, including the seller decision phase for standard auctions. */ + EAuctionStatus status; }; - struct fundaraisingInfo + /** + * @brief Stores all persistent data for one auction. + * @note The same struct is shared by batch and standard auctions. + */ + struct AuctionData { - uint64 tokenPrice; - uint64 soldAmount; - uint64 requiredFunds; - uint64 raisedFunds; - uint32 indexOfProject; - uint32 firstPhaseStartDate; - uint32 firstPhaseEndDate; - uint32 secondPhaseStartDate; - uint32 secondPhaseEndDate; - uint32 thirdPhaseStartDate; - uint32 thirdPhaseEndDate; - uint32 listingStartDate; - uint32 cliffEndDate; - uint32 vestingEndDate; - uint8 threshold; - uint8 TGE; - uint8 stepOfVesting; - bit isCreatedToken; + /** @brief Fields shared with the public auction view. */ + AuctionCore core; + + /** @brief Wallet whitelist used when the private auction uses wallet-based access. */ + HashSet allowedBidderWallets; + + /** @brief Minimum quantity by asset required for participation; owning any one entry grants access. */ + HashMap requiredAccessAssets; }; - struct StateData + /** + * @brief Serializable view of one auction for public getter outputs. + * @note Persistent state uses `HashSet` for access checks, but ABI payloads expose fixed arrays because `HashSet` is not valid in + * input/output structs. + */ + struct AuctionView { - HashMap users; - HashMap, NOSTROMO_MAX_USER> voteStatus; - HashMap numberOfVotedProject; - HashSet tokens; + /** @brief Fields shared with the persistent auction record. */ + AuctionCore core; + + /** @brief Wallet list used when the private auction restricts participation to predefined wallets. */ + Array allowedBidderWallets; + + /** @brief Asset and minimum-quantity alternatives used by private asset-based access. */ + Array requiredAccessAssets; + + /** @brief Number of populated entries in `requiredAccessAssets`. */ + uint64 requiredAccessAssetCount; - HashMap, NOSTROMO_MAX_USER> investors; - HashMap numberOfInvestedProjects; - Array tmpInvestedList; + /** @brief Number of populated entries in `allowedBidderWallets`. */ + uint64 allowedBidderWalletCount; + }; - Array projects; + struct OldStateData + { + struct investInfo + { + uint64 investedAmount; + uint64 claimedAmount; + uint32 indexOfFundraising; + }; - Array fundaraisings; + struct projectInfo + { + id creator; + uint64 tokenName; + uint64 supplyOfToken; + uint32 startDate; + uint32 endDate; + uint32 numberOfYes; + uint32 numberOfNo; + bit isCreatedFundarasing; + }; + + struct fundaraisingInfo + { + uint64 tokenPrice; + uint64 soldAmount; + uint64 requiredFunds; + uint64 raisedFunds; + uint32 indexOfProject; + uint32 firstPhaseStartDate; + uint32 firstPhaseEndDate; + uint32 secondPhaseStartDate; + uint32 secondPhaseEndDate; + uint32 thirdPhaseStartDate; + uint32 thirdPhaseEndDate; + uint32 listingStartDate; + uint32 cliffEndDate; + uint32 vestingEndDate; + uint8 threshold; + uint8 TGE; + uint8 stepOfVesting; + bit isCreatedToken; + }; + + HashMap users; + HashMap, NOSTROMO_MAX_USER_OLD> voteStatus; + HashMap numberOfVotedProject; + HashSet tokens; + + HashMap, NOSTROMO_MAX_USER_OLD> investors; + HashMap numberOfInvestedProjects; + Array tmpInvestedList; + + Array projects; + + Array fundaraisings; id teamAddress; sint64 transferRightsFee; @@ -217,1432 +473,6044 @@ struct NOST : public ContractBase uint32 numberOfRegister, numberOfCreatedProject, numberOfFundraising; }; - struct registerInTier_input + /** + * @brief Epoch fee accrual shared by Nostromo modules. + * @note Auction shareholder amounts are separated by sale tier because their fee formulas differ. Other recipient amounts are compatible sums. + */ + struct NostromoFeePool + { + uint64 shareholderDividendTier1Amount; + uint64 shareholderDividendTier2Amount; + uint64 shareholderDividendTier3Amount; + uint64 shareholderDividendTier4Amount; + uint64 commonServiceFeeAmount; + uint64 shareholderDividendAmount; + uint64 managementAmount; + uint64 developmentAmount; + uint64 takeoverCoordinatorAmount; + }; + + struct StateData + { + /** @brief Configured fee charged when creating a private auction. */ + sint64 privateAuctionFee; + + /** @brief Configured non-negative fee accumulated when creating a public auction and distributed at `END_EPOCH`. */ + sint64 publicAuctionCreationFee; + + /** @brief Configured cancellation fee rate in basis points. */ + uint64 auctionCancellationFeeBasisPoints; + + /** @brief Undistributed shareholder revenue from the shared fee pool reserved for contract dividends. */ + uint64 auctionShareholderDividendPool; + + /** @brief Configured management fee rate in basis points, charged from auction proceeds. */ + uint64 managementFeeBasisPoints; + + /** @brief Configured development fee rate in basis points, charged from auction proceeds. */ + uint64 developmentFeeBasisPoints; + + /** @brief Configured takeover coordinator fee rate in basis points, charged from auction proceeds. */ + uint64 takeoverCoordinatorFeeBasisPoints; + + /** @brief Share of the shareholder fee redirected to dividends, expressed in basis points. */ + uint64 shareholderDividendBasisPoints; + + /** @brief Shareholder fee tier applied to auctions up to the first threshold. */ + uint64 shareholderFeeBasisPointsTier1; + + /** @brief Shareholder fee tier applied to auctions above the first threshold and up to the second threshold. */ + uint64 shareholderFeeBasisPointsTier2; + + /** @brief Shareholder fee tier applied to auctions above the second threshold and up to the third threshold. */ + uint64 shareholderFeeBasisPointsTier3; + + /** @brief Shareholder fee tier applied to auctions above the third threshold. */ + uint64 shareholderFeeBasisPointsTier4; + + /** @brief Start of the currently active global auction timer pause interval. */ + DateAndTime auctionTimerPauseStartedAt; + + /** @brief End of the currently active global auction timer pause interval. */ + DateAndTime auctionTimerPauseEndsAt; + + /** @brief Configured maximum auction duration in days. */ + uint32 maxAuctionDurationDays; + + /** @brief Cached QX transfer fee refreshed at the beginning of each epoch. */ + uint32 qxTransferFee; + + /** @brief Flag indicating whether the post-`BEGIN_EPOCH()` auction pause is active for the current epoch. */ + uint8 isPostBeginEpochPauseArmed; + + /** @brief Flag indicating whether auction deadlines are currently frozen by a global pause interval. */ + uint8 isAuctionTimerPaused; + + /** @brief Flag indicating whether every auction fee is routed to the development wallet. */ + uint8 routeAllFeesToDevelopment; + + id management; + + id development; + + id takeoverCoordinator; + + /** @brief Total number of auctions ever created; also the next auction index. */ + uint64 totalAuctionsCreated; + + /** @brief Circular buffer with full snapshots of finalized and cancelled auctions. */ + Array closedAuctionHistory; + + /** @brief Monotonic insertion counter for `closedAuctionHistory`. */ + uint64 closedAuctionHistoryCounter; + + HashMap auctionList; + /** @brief Active bid records; slots are cleared as soon as the bid leaves the live order book. */ + Array participants; + /** @brief Bounded history of completed, refunded, and displaced bid records. */ + Array participantHistory; + /** @brief Monotonic insertion counter for `participantHistory`. */ + uint64 participantHistoryCounter; + + /** @brief Wallet-indexed QU liabilities registered before an auction is finalized. */ + HashMap pendingQuPayouts; + /** @brief Sum of all values in `pendingQuPayouts`, in qu. */ + uint64 totalPendingQuPayouts; + /** @brief Physical hash-map slot from which the next bounded automatic payout scan starts. */ + uint64 pendingPayoutScanCursor; + /** @brief Lifetime number of finalized auctions. */ + uint64 totalFinalizedAuctions; + /** @brief Lifetime number of cancelled auctions. */ + uint64 totalCancelledAuctions; + + /** @brief Shared fee accrual for Auction House and future Nostromo modules, settled at `END_EPOCH`. */ + NostromoFeePool feePool; + + /** @brief Configured drop in the execution fee reserve that triggers an emergency pause, in basis points. */ + uint64 feeReserveGuardDropBasisPoints; + + /** @brief Configured rolling window used to evaluate the execution fee reserve drop, in seconds. */ + uint64 feeReserveGuardWindowSeconds; + + /** @brief Execution fee reserve value recorded at the start of the current guard window. */ + sint64 feeReserveBaseline; + + /** @brief Start of the current guard window; invalid when the window has not been initialized. */ + DateAndTime feeReserveBaselineAt; + + /** @brief Timestamp at which the emergency pause was triggered; invalid when not paused. */ + DateAndTime emergencyPausedAt; + + /** @brief Flag indicating whether an emergency pause is currently blocking auction interactions. */ + uint8 isEmergencyPaused; + }; + + /** @brief Input payload used to create a Batch Auction or Standard Auction in the Auction House. */ + struct CreateAuction_input { - uint32 tierLevel; + /** @brief Lowercase base32 CIDv1 stored in Pinata for the auction name and description metadata. */ + Array metadataIpfsCid; + + /** @brief Assets and quantities offered by the auction. */ + Array auctionLotItems; + + /** @brief Asset and minimum-quantity alternatives used by private asset-based access. */ + Array requiredAccessAssets; + + /** @brief Wallet list used when the private auction restricts participation to predefined wallets. */ + Array allowedBidderWallets; + + /** @brief Required minimum requested quantity for batch bids; ignored for standard auctions. */ + uint64 minimumPurchaseQuantity; + + /** @brief Initial price for a standard auction; bids cannot be placed below this total price for the whole lot. */ + uint64 initialPrice; + + /** @brief Minimum selling price per asset in a batch auction, or desired minimum total selling price for the whole lot in a standard + * auction. */ + uint64 salePrice; + + /** @brief Minimum increment by which each new standard auction bid must exceed the current highest bid. */ + uint64 minimumBidIncrement; + + /** @brief Buy Now price that immediately closes a standard auction once matched or exceeded. */ + uint64 buyNowPrice; + + /** @brief Auction duration in days, capped by the contract configuration. */ + uint32 durationDays; + + /** @brief Auction House mode selected by the seller: Batch Auction or Standard Auction. */ + uint8 auctionType; + + /** @brief Visibility selected by the seller: public or private. */ + uint8 auctionVisibility; }; - struct registerInTier_output + /** @brief Result of auction creation. */ + struct CreateAuction_output { - uint32 tierLevel; + /** @brief Monotonic index assigned to the new auction when creation succeeds. */ + uint64 auctionIndex; + + /** @brief Result code describing whether the auction creation succeeded. */ + EAuctionError errorCode; }; - struct logoutFromTier_input + /** @brief Input payload used to place a bid in a Batch Auction or Standard Auction. */ + struct PlaceBid_input { + /** @brief Monotonic index of the target auction. */ + uint64 auctionIndex; + + /** @brief Requested quantity for a batch auction, which must meet its configured minimum; ignored for a standard auction. */ + uint64 quantity; + /** @brief Offered price per asset in a batch auction, or total offered price for the whole lot in a standard auction. */ + uint64 bidAmount; }; - struct logoutFromTier_output + /** @brief Result of a bid placement request. */ + struct PlaceBid_output { - bit result; + /** @brief Amount that remains escrowed for the accepted bid. */ + uint64 escrowedAmount; + + /** @brief Amount refunded to the bidder, including replaced escrow or invocation change. */ + uint64 refundedAmount; + + /** @brief Result code describing whether the bid placement succeeded. */ + EAuctionError errorCode; }; - struct createProject_input + /** @brief Input payload used to cancel an active auction. */ + struct CancelAuction_input { - uint64 tokenName; - uint64 supply; - uint32 startYear; - uint32 startMonth; - uint32 startDay; - uint32 startHour; - uint32 endYear; - uint32 endMonth; - uint32 endDay; - uint32 endHour; + /** @brief Monotonic index of the auction that the seller wants to cancel. */ + uint64 auctionIndex; }; - struct createProject_output + /** @brief Result of an auction cancellation request. */ + struct CancelAuction_output { - uint32 indexOfProject; + /** @brief Total amount refunded to bidders because of the cancellation. */ + uint64 refundedAmount; + + /** @brief Cancellation fee charged to the seller according to the auction rules. */ + uint64 cancellationFee; + + /** @brief Result code describing whether the cancellation succeeded. */ + EAuctionError errorCode; }; - struct voteInProject_input + /** @brief Input payload used by the seller to accept or reject a pending standard auction result. */ + struct ResolvePendingStandardAuction_input { - uint32 indexOfProject; - bit decision; + /** @brief Monotonic index of the standard auction awaiting the seller decision. */ + uint64 auctionIndex; + + /** @brief Set to `1` to accept the sale or `0` to reject it. */ + uint8 acceptSale; }; - struct voteInProject_output + /** @brief Result of a seller decision on a pending standard auction. */ + struct ResolvePendingStandardAuction_output { + /** @brief Amount refunded to the bidder when the seller rejects the sale. */ + uint64 refundedAmount; + /** @brief Result code describing whether the seller decision was applied. */ + EAuctionError errorCode; }; - struct createFundraising_input + /** @brief Input payload used by the takeover coordinator to overwrite the full auction fee configuration. */ + struct SetAuctionFees_input { - uint64 tokenPrice; - uint64 soldAmount; - uint64 requiredFunds; + /** @brief Fee charged when a private auction is created. */ + sint64 privateAuctionFee; - uint32 indexOfProject; - uint32 firstPhaseStartYear; - uint32 firstPhaseStartMonth; - uint32 firstPhaseStartDay; - uint32 firstPhaseStartHour; - uint32 firstPhaseEndYear; - uint32 firstPhaseEndMonth; - uint32 firstPhaseEndDay; - uint32 firstPhaseEndHour; + /** @brief Non-negative fee accumulated when a public auction is created and distributed at `END_EPOCH`. */ + sint64 publicAuctionCreationFee; - uint32 secondPhaseStartYear; - uint32 secondPhaseStartMonth; - uint32 secondPhaseStartDay; - uint32 secondPhaseStartHour; - uint32 secondPhaseEndYear; - uint32 secondPhaseEndMonth; - uint32 secondPhaseEndDay; - uint32 secondPhaseEndHour; + /** @brief Cancellation fee rate in basis points. */ + uint64 auctionCancellationFeeBasisPoints; - uint32 thirdPhaseStartYear; - uint32 thirdPhaseStartMonth; - uint32 thirdPhaseStartDay; - uint32 thirdPhaseStartHour; - uint32 thirdPhaseEndYear; - uint32 thirdPhaseEndMonth; - uint32 thirdPhaseEndDay; - uint32 thirdPhaseEndHour; + /** @brief Management fee rate in basis points. */ + uint64 managementFeeBasisPoints; - uint32 listingStartYear; - uint32 listingStartMonth; - uint32 listingStartDay; - uint32 listingStartHour; + /** @brief Development fee rate in basis points. */ + uint64 developmentFeeBasisPoints; - uint32 cliffEndYear; - uint32 cliffEndMonth; - uint32 cliffEndDay; - uint32 cliffEndHour; + /** @brief Takeover coordinator fee rate in basis points. */ + uint64 takeoverCoordinatorFeeBasisPoints; - uint32 vestingEndYear; - uint32 vestingEndMonth; - uint32 vestingEndDay; - uint32 vestingEndHour; + /** @brief Percentage of the shareholder fee distributed as dividends, in basis points. */ + uint64 shareholderDividendBasisPoints; - uint8 threshold; - uint8 TGE; - uint8 stepOfVesting; - }; + /** @brief Shareholder fee tier for auctions up to the first threshold. */ + uint64 shareholderFeeBasisPointsTier1; - struct createFundraising_output - { + /** @brief Shareholder fee tier for auctions above the first threshold and up to the second threshold. */ + uint64 shareholderFeeBasisPointsTier2; + + /** @brief Shareholder fee tier for auctions above the second threshold and up to the third threshold. */ + uint64 shareholderFeeBasisPointsTier3; + /** @brief Shareholder fee tier for auctions above the third threshold. */ + uint64 shareholderFeeBasisPointsTier4; }; - struct investInProject_input + struct SetAuctionFees_output { - uint32 indexOfFundraising; + /** @brief Result code describing whether the fee update succeeded. */ + EAuctionError errorCode; }; - struct investInProject_output + /** @brief Input payload used by management to update every fee except takeover coordinator-specific splits. */ + struct SetAuctionFeesByManagement_input { + /** @brief Fee charged when a private auction is created. */ + sint64 privateAuctionFee; + /** @brief Non-negative fee accumulated when a public auction is created and distributed at `END_EPOCH`. */ + sint64 publicAuctionCreationFee; + + /** @brief Cancellation fee rate in basis points. */ + uint64 auctionCancellationFeeBasisPoints; + + /** @brief Management fee rate in basis points. */ + uint64 managementFeeBasisPoints; + + /** @brief Development fee rate in basis points. */ + uint64 developmentFeeBasisPoints; + + /** @brief Shareholder fee tier for auctions up to the first threshold. */ + uint64 shareholderFeeBasisPointsTier1; + + /** @brief Shareholder fee tier for auctions above the first threshold and up to the second threshold. */ + uint64 shareholderFeeBasisPointsTier2; + + /** @brief Shareholder fee tier for auctions above the second threshold and up to the third threshold. */ + uint64 shareholderFeeBasisPointsTier3; + + /** @brief Shareholder fee tier for auctions above the third threshold. */ + uint64 shareholderFeeBasisPointsTier4; }; - struct claimToken_input + struct SetAuctionFeesByManagement_output { - uint64 amount; - uint32 indexOfFundraising; + /** @brief Result code describing whether the fee update succeeded. */ + EAuctionError errorCode; }; - struct claimToken_output + /** @brief Input payload used by the takeover coordinator to appoint a new management wallet. */ + struct SetManagement_input { - uint64 claimedAmount; + /** @brief New wallet that will receive management privileges. */ + id management; }; - struct upgradeTier_input + struct SetManagement_output { - uint32 newTierLevel; + /** @brief Result code describing whether the management update succeeded. */ + EAuctionError errorCode; }; - struct upgradeTier_output + /** @brief Input payload used by the takeover coordinator or management to configure the execution fee reserve guard. */ + struct SetFeeReserveGuardConfig_input { + /** @brief Drop in the execution fee reserve, relative to the window baseline, that triggers an emergency pause, in basis points. */ + uint64 dropBasisPoints; + /** @brief Rolling window used to evaluate the execution fee reserve drop, in seconds. */ + uint64 windowSeconds; }; - struct TransferShareManagementRights_input + struct SetFeeReserveGuardConfig_output { - Asset asset; - sint64 numberOfShares; - uint32 newManagingContractIndex; + /** @brief Result code describing whether the guard configuration update succeeded. */ + EAuctionError errorCode; }; - struct TransferShareManagementRights_output + + /** @brief Input payload used by the takeover coordinator or management to manually pause or resume auction interactions. */ + struct SetEmergencyPause_input { - sint64 transferredNumberOfShares; + /** @brief Set to `1` to activate the emergency pause or `0` to resume normal operation. */ + uint8 paused; }; - struct getStats_input + struct SetEmergencyPause_output { - + /** @brief Result code describing whether the emergency pause update succeeded. */ + EAuctionError errorCode; }; - struct getStats_output + /** @brief Input payload used to fetch one auction from storage. */ + struct GetAuctionByIndex_input { - uint64 epochRevenue, totalPoolWeight; - uint32 numberOfRegister, numberOfCreatedProject, numberOfFundraising; + /** @brief Monotonic index of the auction to read. */ + uint64 auctionIndex; }; - struct getTierLevelByUser_input + /** @brief Auction data returned by the read-only auction getter. */ + struct GetAuctionByIndex_output { - id userId; + /** @brief Serializable auction data stored for the requested auction. */ + AuctionView auction; + + /** @brief Flag indicating whether the auction record exists. */ + uint8 found; }; - struct getTierLevelByUser_output + /** @brief Input payload used to fetch one participant record from an auction. */ + struct GetAuctionParticipant_input { - uint8 tierLevel; + /** @brief Monotonic index of the auction that owns the participant record. */ + uint64 auctionIndex; + + /** @brief Wallet whose participant record should be returned. */ + id participant; }; - struct getUserVoteStatus_input + /** @brief Participant data returned by the read-only participant getter. */ + struct GetAuctionParticipant_output { - id userId; + /** @brief Participant record for the requested wallet in the requested auction. */ + AuctionParticipantData participantData; + + /** @brief Flag indicating whether the participant record exists. */ + uint8 found; }; - struct getUserVoteStatus_output + /** @brief Input payload used to query the remaining post-BEGIN_EPOCH auction launch pause. */ + using GetTicksBeforeAuctionLaunch_input = NoData; + + /** @brief Result returned by the auction launch pause getter. */ + struct GetTicksBeforeAuctionLaunch_output { - uint32 numberOfVotedProjects; - Array projectIndexList; + /** @brief Number of ticks remaining before auction interactions resume after `BEGIN_EPOCH`. */ + uint32 ticks; }; - struct checkTokenCreatability_input + /** @brief Input payload used to read the current auction fee configuration. */ + /** @brief Input payload used to read the amount of accumulated service fees awaiting distribution at `END_EPOCH`. */ + using GetPendingServiceFeePool_input = NoData; + + struct GetPendingServiceFeePool_output { - uint64 tokenName; + /** @brief Aggregate fee amount still awaiting `END_EPOCH` settlement. */ + uint64 pendingServiceFeePool; }; - struct checkTokenCreatability_output + /** @brief Input payload used to inspect the detailed shared Nostromo fee pool. */ + using GetNostromoFeePool_input = NoData; + + struct GetNostromoFeePool_output { - bit result; // result = 1 is the token already issued by SC + /** @brief Detailed fee accumulators that have not yet been moved to dividends or recipient payout liabilities. */ + NostromoFeePool feePool; + + /** @brief Aggregate of every amount in `feePool`. */ + uint64 totalAmount; }; - struct getNumberOfInvestedProjects_input + /** @brief Input used to inspect a wallet's registered QU payout. */ + struct GetPendingPayout_input { - id userId; + /** @brief Wallet whose unpaid QU amount should be returned. */ + id account; }; - struct getNumberOfInvestedProjects_output + struct GetPendingPayout_output { - uint32 numberOfInvestedProjects; + /** @brief QU currently owed to the requested wallet. */ + uint64 amount; }; -protected: + /** @brief Input payload used to read the current state of the execution fee reserve guard. */ + using GetFeeReserveGuardState_input = NoData; - struct registerInTier_locals + struct GetFeeReserveGuardState_output { - uint64 tierStakedAmount; - uint32 poolWeight; + /** @brief Live execution fee reserve value read from the system contract. */ + sint64 currentFeeReserve; + + /** @brief Execution fee reserve value recorded at the start of the current guard window. */ + sint64 feeReserveBaseline; + + /** @brief Start of the current guard window; invalid when the window has not been initialized. */ + DateAndTime feeReserveBaselineAt; + + /** @brief Timestamp at which the emergency pause was triggered; invalid when not paused. */ + DateAndTime emergencyPausedAt; + + /** @brief Configured drop in the execution fee reserve that triggers an emergency pause, in basis points. */ + uint64 dropBasisPoints; + + /** @brief Configured rolling window used to evaluate the execution fee reserve drop, in seconds. */ + uint64 windowSeconds; + + /** @brief Flag indicating whether an emergency pause is currently blocking auction interactions. */ + uint8 isEmergencyPaused; }; - PUBLIC_PROCEDURE_WITH_LOCALS(registerInTier) - { - if (state.get().users.contains(qpi.invocator())) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - if (input.tierLevel < 1 || input.tierLevel > 5) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - - switch (input.tierLevel) - { - case 1: - locals.tierStakedAmount = NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT; - locals.poolWeight = NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT; - break; - case 2: - locals.tierStakedAmount = NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT; - locals.poolWeight = NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; - break; - case 3: - locals.tierStakedAmount = NOSTROMO_TIER_DOG_STAKE_AMOUNT; - locals.poolWeight = NOSTROMO_TIER_DOG_POOL_WEIGHT; - break; - case 4: - locals.tierStakedAmount = NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT; - locals.poolWeight = NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; - break; - case 5: - locals.tierStakedAmount = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT; - locals.poolWeight = NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; - break; - default: - break; - } - if (qpi.invocationReward() < (sint64)locals.tierStakedAmount) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - else - { - state.mut().users.set(qpi.invocator(), input.tierLevel); - state.mut().numberOfRegister++; - if (qpi.invocationReward() > (sint64)locals.tierStakedAmount) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.tierStakedAmount); - } - state.mut().totalPoolWeight += locals.poolWeight; - output.tierLevel = input.tierLevel; - } - } + using GetAuctionFees_input = NoData; - struct logoutFromTier_locals + struct GetAuctionFees_output { - uint64 earnedAmount; - uint32 elementIndex; - uint8 tierLevel; + /** @brief Fee charged when a private auction is created. */ + sint64 privateAuctionFee; + + /** @brief Non-negative fee accumulated when a public auction is created and distributed at `END_EPOCH`. */ + sint64 publicAuctionCreationFee; + + /** @brief Cancellation fee rate in basis points. */ + uint64 auctionCancellationFeeBasisPoints; + + /** @brief Management fee rate in basis points. */ + uint64 managementFeeBasisPoints; + + /** @brief Development fee rate in basis points. */ + uint64 developmentFeeBasisPoints; + + /** @brief Takeover coordinator fee rate in basis points. */ + uint64 takeoverCoordinatorFeeBasisPoints; + + /** @brief Percentage of the shareholder fee distributed as dividends, in basis points. */ + uint64 shareholderDividendBasisPoints; + + /** @brief Shareholder fee tier for auctions up to the first threshold. */ + uint64 shareholderFeeBasisPointsTier1; + + /** @brief Shareholder fee tier for auctions above the first threshold and up to the second threshold. */ + uint64 shareholderFeeBasisPointsTier2; + + /** @brief Shareholder fee tier for auctions above the second threshold and up to the third threshold. */ + uint64 shareholderFeeBasisPointsTier3; + + /** @brief Shareholder fee tier for auctions above the third threshold. */ + uint64 shareholderFeeBasisPointsTier4; }; - PUBLIC_PROCEDURE_WITH_LOCALS(logoutFromTier) + /** @brief Input for the arithmetic-only Batch Auction bid reward calculator. */ + struct CalculateBatchAuctionBidFee_input { - if (state.get().users.contains(qpi.invocator()) == 0) - { - return ; - } - state.get().users.get(qpi.invocator(), locals.tierLevel); - switch (locals.tierLevel) - { - case 1: - locals.earnedAmount = div(NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT * NOSTROMO_TIER_FACEHUGGER_UNSTAKE_FEE, 100ULL); - qpi.transfer(qpi.invocator(), qpi.invocationReward() + NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT - locals.earnedAmount); - state.mut().epochRevenue += locals.earnedAmount; - state.mut().totalPoolWeight -= NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT; - break; - case 2: - locals.earnedAmount = div(NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT * NOSTROMO_TIER_CHESTBURST_UNSTAKE_FEE, 100ULL); - qpi.transfer(qpi.invocator(), qpi.invocationReward() + NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT - locals.earnedAmount); - state.mut().epochRevenue += locals.earnedAmount; - state.mut().totalPoolWeight -= NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; - break; - case 3: - locals.earnedAmount = div(NOSTROMO_TIER_DOG_STAKE_AMOUNT * NOSTROMO_TIER_DOG_UNSTAKE_FEE, 100ULL); - qpi.transfer(qpi.invocator(), qpi.invocationReward() + NOSTROMO_TIER_DOG_STAKE_AMOUNT - locals.earnedAmount); - state.mut().epochRevenue += locals.earnedAmount; - state.mut().totalPoolWeight -= NOSTROMO_TIER_DOG_POOL_WEIGHT; - break; - case 4: - locals.earnedAmount = div(NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT * NOSTROMO_TIER_XENOMORPH_UNSTAKE_FEE, 100ULL); - qpi.transfer(qpi.invocator(), qpi.invocationReward() + NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT - locals.earnedAmount); - state.mut().epochRevenue += locals.earnedAmount; - state.mut().totalPoolWeight -= NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; - break; - case 5: - locals.earnedAmount = div(NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT * NOSTROMO_TIER_WARRIOR_UNSTAKE_FEE, 100ULL); - qpi.transfer(qpi.invocator(), qpi.invocationReward() + NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT - locals.earnedAmount); - state.mut().epochRevenue += locals.earnedAmount; - state.mut().totalPoolWeight -= NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; - break; - default: - break; - } + /** @brief Number of assets requested by the prospective bid. */ + uint64 bidQuantity; - state.mut().users.removeByKey(qpi.invocator()); - state.mut().numberOfRegister -= 1; - output.result = 1; - } + /** @brief Prospective price per asset, in qu. */ + uint64 bidAmount; + }; - struct createProject_locals + /** @brief Escrow, accumulated fee, and total reward required by the Batch Auction bid arithmetic. */ + struct CalculateBatchAuctionBidFee_output { - projectInfo newProject; - uint32 elementIndex, startDate, endDate, curDate; - uint8 tierLevel; + /** @brief Saturating product of `bidQuantity` and `bidAmount`. */ + uint64 escrowAmount; + + /** @brief Amount accumulated for distribution at `END_EPOCH` for an accepted bid: `max(100 - bidQuantity * bidAmount, 0)`. */ + uint64 fee; + + /** @brief Saturating sum of `escrowAmount` and `fee`. */ + uint64 requiredReward; }; - PUBLIC_PROCEDURE_WITH_LOCALS(createProject) + /** + * @brief Pure breakdown of one auction revenue split. + * @note Runtime settlement and tests share this struct to keep fee arithmetic aligned. + */ + struct AuctionRevenueBreakdown { - packNostromoDate(input.startYear, input.startMonth, input.startDay, input.startHour, 0, 0, locals.startDate); - packNostromoDate(input.endYear, input.endMonth, input.endDay, input.endHour, 0, 0, locals.endDate); - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); + /** @brief Net amount that remains for the seller after every configured fee is applied. */ + uint64 sellerPayout; - if(locals.curDate > locals.startDate || locals.startDate >= locals.endDate || checkValidNostDateTime(locals.startDate) == 0 || checkValidNostDateTime(locals.endDate) == 0) - { - output.indexOfProject = NOSTROMO_MAX_NUMBER_PROJECT; - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } + /** @brief Shareholder fee tier selected for the provided gross amount. */ + uint64 shareholderFeeBasisPoints; - if (state.get().tokens.contains(input.tokenName)) - { - output.indexOfProject = NOSTROMO_MAX_NUMBER_PROJECT; - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } + /** @brief Gross shareholder fee amount before dividend retention is split out. */ + uint64 shareholderFeeAmount; - if (state.get().users.get(qpi.invocator(), locals.tierLevel) && (locals.tierLevel == 4 || locals.tierLevel == 5)) - { - if (qpi.invocationReward() < NOSTROMO_CREATE_PROJECT_FEE) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.indexOfProject = NOSTROMO_MAX_NUMBER_PROJECT; - return ; + /** @brief Portion of the shareholder fee retained by the contract for dividend distribution. */ + uint64 shareholderDividendAmount; + + /** @brief Management wallet fee amount. */ + uint64 managementFeeAmount; + + /** @brief Development wallet fee amount. */ + uint64 developmentFeeAmount; + + /** @brief Base takeover coordinator fee charged directly from the gross amount. */ + uint64 takeoverCoordinatorBaseAmount; + + /** @brief Total takeover coordinator gain including retained shareholder-fee remainder. */ + uint64 takeoverCoordinatorFeeAmount; + }; + + /** + * @brief Pure breakdown of one service fee charged for private-auction creation or auction cancellation. + * @note Runtime settlement and tests share this struct to keep fee arithmetic aligned. + */ + struct AuctionServiceFeeBreakdown + { + /** @brief Portion retained by the contract for shareholder dividends. */ + uint64 shareholderDividendAmount; + + /** @brief Management wallet fee amount. */ + uint64 managementFeeAmount; + + /** @brief Development wallet fee amount. */ + uint64 developmentFeeAmount; + + /** @brief Takeover coordinator wallet fee amount. */ + uint64 takeoverCoordinatorFeeAmount; + }; + + /** @brief Input payload used to read the wallets that receive auction fee transfers. */ + using GetFeeRecipients_input = NoData; + + struct GetFeeRecipients_output + { + /** @brief Wallet that receives the management fee. */ + id management; + + /** @brief Wallet that receives the development fee. */ + id development; + + /** @brief Wallet that receives the takeover coordinator fee. */ + id takeoverCoordinator; + }; + + /** @brief Input payload used to read the closed auctions history ring buffer. */ + using GetClosedAuctionHistory_input = NoData; + + struct GetClosedAuctionHistory_output + { + /** @brief Ring buffer of auction indices recorded after finalization or cancellation. */ + Array auctionIndices; + + /** @brief Total number of history writes since initialization. */ + uint64 totalEntries; + }; + + /** @brief Input payload used to read the temporary fee routing override flag. */ + using GetRouteAllFeesToDevelopment_input = NoData; + + struct GetRouteAllFeesToDevelopment_output + { + /** @brief `1` routes every fee to development, `0` uses the standard fee distribution. */ + uint8 enabled; + }; + + struct AuctionSummary + { + Array metadataIpfsCid; + id seller; + id highestBidder; + DateAndTime createdAt; + DateAndTime settledAt; + uint64 auctionIndex; + uint64 quantityForSale; + uint64 allocatedQuantity; + uint64 initialPrice; + uint64 salePrice; + uint64 buyNowPrice; + uint64 highestBidPrice; + uint64 highestBidQuantity; + uint64 highestBidAmount; + uint8 type; + uint8 visibility; + uint8 status; + }; + + struct ParticipantSummary + { + id participant; + DateAndTime lastBidTime; + uint64 bidAmount; + uint64 escrowedAmount; + uint64 requestedQuantity; + uint64 allocatedQuantity; + uint8 isWinningBid; + }; + + struct UserParticipationSummary + { + id participant; + DateAndTime lastBidTime; + uint64 auctionIndex; + uint64 bidAmount; + uint64 escrowedAmount; + uint64 requestedQuantity; + uint64 allocatedQuantity; + uint8 isWinningBid; + }; + + struct ContractStats + { + uint64 totalAuctionsCreated; + uint64 activeAuctionCount; + uint64 pendingSellerDecisionAuctionCount; + uint64 finalizedAuctionCount; + uint64 cancelledAuctionCount; + uint64 participantCount; + uint64 closedAuctionHistoryCounter; + uint64 auctionShareholderDividendPool; + uint64 pendingServiceFeePool; + uint64 totalPendingQuPayouts; + uint64 retainedClosedAuctionCount; + uint64 retainedParticipantHistoryCount; + uint32 qxTransferFee; + uint8 routeAllFeesToDevelopment; + uint8 isAuctionTimerPaused; + uint8 isPostBeginEpochPauseArmed; + uint8 isEmergencyPaused; + }; + + using GetContractStats_input = NoData; + struct GetContractStats_output + { + ContractStats stats; + }; + + struct GetAuctionSummaries_input + { + uint64 offset; + uint64 limit; + }; + struct GetAuctionSummaries_output + { + Array auctions; + uint64 totalCount; + uint64 returnedCount; + }; + + struct GetActiveAuctionIndices_input + { + uint64 offset; + uint64 limit; + }; + struct GetActiveAuctionIndices_output + { + Array auctionIndices; + uint64 totalCount; + uint64 returnedCount; + }; + + struct GetAuctionsBySeller_input + { + id seller; + uint64 offset; + uint64 limit; + }; + struct GetAuctionsBySeller_output + { + Array auctions; + uint64 totalCount; + uint64 returnedCount; + }; + + struct GetAuctionByMetadataCid_input + { + Array metadataIpfsCid; + }; + struct GetAuctionByMetadataCid_output + { + AuctionSummary auction; + uint64 auctionIndex; + uint8 found; + }; + + struct GetAuctionSummariesByIndexBatch_input + { + Array auctionIndices; + uint64 count; + }; + struct GetAuctionSummariesByIndexBatch_output + { + Array auctions; + Array found; + uint64 returnedCount; + }; + + struct GetAuctionParticipants_input + { + uint64 auctionIndex; + uint64 offset; + uint64 limit; + }; + struct GetAuctionParticipants_output + { + Array participants; + uint64 totalCount; + uint64 returnedCount; + }; + + struct GetUserParticipations_input + { + id participant; + uint64 offset; + uint64 limit; + }; + struct GetUserParticipations_output + { + Array participations; + uint64 totalCount; + uint64 returnedCount; + }; + + using GetLatestAuctionIndex_input = NoData; + struct GetLatestAuctionIndex_output + { + uint64 auctionIndex; + uint8 found; + }; + + struct GetAuctionCountBySeller_input + { + id seller; + }; + struct GetAuctionCountBySeller_output + { + uint64 count; + }; + + struct GetAuctionAtCreationSnapshot_input + { + uint64 auctionIndex; + }; + struct GetAuctionAtCreationSnapshot_output + { + id seller; + DateAndTime createdAt; + uint64 auctionIndex; + uint64 quantityForSale; + uint64 initialPrice; + uint64 salePrice; + uint64 minimumBidIncrement; + uint64 buyNowPrice; + uint64 auctionDurationSeconds; + uint8 type; + uint8 visibility; + uint8 found; + }; + + /** @brief Input payload used to read current bid capacity guidance for one active Batch Auction. */ + struct GetBatchAuctionBidAvailability_input + { + /** @brief Monotonic index of the Batch Auction to inspect. */ + uint64 auctionIndex; + }; + + /** @brief Read-only guidance for the next acceptable Batch Auction bid. */ + struct GetBatchAuctionBidAvailability_output + { + /** @brief Lowest price per asset that can currently accept a new bid meeting the auction minimum quantity. */ + uint64 minimumBidPrice; + + /** @brief Quantity available at `minimumBidPrice`; zero when no valid new bid can be accepted. */ + uint64 availableQuantity; + + /** @brief Flag indicating whether the auction exists. */ + uint8 found; + + /** @brief Flag indicating whether the auction is an active Batch Auction that can accept another valid bid. */ + uint8 isAcceptingBids; + }; + + /** @brief Internal input used to validate an auction lot and resolve its total escrow quantity. */ + struct AnalyzeAuctionLot_input + { + /** @brief Auction lot contents to validate. */ + Array auctionLotItems; + + /** @brief Requested auction duration in days. */ + uint32 durationDays; + }; + + /** @brief Internal output returned after validating an auction lot. */ + struct AnalyzeAuctionLot_output + { + /** @brief Total quantity that must be escrowed from the lot. */ + uint64 totalEscrowQuantity; + + /** @brief Number of non-empty lot entries found in the lot. */ + uint64 lotItemCount; + + /** @brief Flag indicating whether the lot and duration are valid. */ + uint8 isValid; + }; + + struct AnalyzeAuctionLot_locals + { + AuctionAssetEntry lotItem; + uint64 lotItemIndex; + }; + + /** @brief Internal input used to count non-empty wallet entries in a private wallet whitelist. */ + struct CountAllowedBidderWallets_input + { + /** @brief Wallet list provided for private wallet-based access control. */ + Array allowedBidderWallets; + }; + + /** @brief Internal output containing the number of non-empty wallet whitelist entries. */ + struct CountAllowedBidderWallets_output + { + /** @brief Number of non-zero wallet entries found in the whitelist. */ + uint64 allowedWalletCount; + }; + + struct CountAllowedBidderWallets_locals + { + uint64 allowedWalletIndex; + }; + + /** @brief Internal input used to count non-empty asset entries in a private asset access list. */ + struct CountRequiredAccessAssets_input + { + /** @brief Asset and minimum-quantity alternatives provided for private access control. */ + Array requiredAccessAssets; + }; + + /** @brief Internal output containing the number of non-empty private access assets. */ + struct CountRequiredAccessAssets_output + { + /** @brief Number of populated asset entries found in the private access list. */ + uint64 requiredAccessAssetCount; + + /** @brief Flag indicating whether every entry has a valid asset/quantity combination. */ + uint8 isValid; + }; + + struct CountRequiredAccessAssets_locals + { + AuctionAssetEntry requiredAccessAsset; + uint64 requiredAccessAssetIndex; + }; + + struct NostromoProcedureLog + { + uint32 contractIndex; + uint32 errorCode; + id actor; + sint64 amount; + uint64 auctionIndex; + uint8 procedure; + sint8 _terminator; + }; + + /** @brief Internal input used to locate either a live or retained closed auction. */ + struct FindAuction_input + { + uint64 auctionIndex; + }; + + struct FindAuction_output + { + AuctionData auction; + uint8 found; + }; + + struct FindAuction_locals + { + AuctionData archivedAuction; + uint64 historyIndex; + }; + + /** @brief Internal input used to test whether an auction remains in retained closed history. */ + struct IsClosedAuctionRetained_input + { + uint64 auctionIndex; + }; + + struct IsClosedAuctionRetained_output + { + uint8 found; + }; + + struct IsClosedAuctionRetained_locals + { + uint64 retainedClosedAuctionCount; + uint64 historyIndex; + }; + + /** @brief Internal cursor used to enumerate retained auctions in ascending creation order. */ + struct SelectNextRetainedAuction_input + { + id seller; + uint64 afterAuctionIndex; + uint8 hasAfterAuctionIndex; + uint8 includeClosedAuctions; + uint8 filterBySeller; + }; + + struct SelectNextRetainedAuction_output + { + AuctionData auction; + uint8 found; + }; + + struct SelectNextRetainedAuction_locals + { + AuctionData candidateAuction; + uint64 retainedClosedAuctionCount; + uint64 historyIndex; + sint64 auctionElementIndex; + }; + + struct CountRetainedAuctionsBySeller_input + { + id seller; + }; + + struct CountRetainedAuctionsBySeller_output + { + uint64 count; + }; + + struct CountRetainedAuctionsBySeller_locals + { + AuctionData candidateAuction; + uint64 retainedClosedAuctionCount; + uint64 historyIndex; + sint64 auctionElementIndex; + }; + + struct FindFirstRetainedAuctionByMetadataCid_input + { + Array metadataIpfsCid; + }; + + struct FindFirstRetainedAuctionByMetadataCid_output + { + AuctionData auction; + uint8 found; + }; + + struct FindFirstRetainedAuctionByMetadataCid_locals + { + AuctionData candidateAuction; + uint64 retainedClosedAuctionCount; + uint64 metadataIndex; + uint64 historyIndex; + sint64 auctionElementIndex; + uint8 metadataMatches; + }; + + struct GetAuctionByIndex_locals + { + AuctionData auction; + FindAuction_input findAuctionInput; + FindAuction_output findAuctionOutput; + AuctionAssetEntry requiredAccessAsset; + id allowedBidderWallet; + sint64 requiredAccessAssetSetIndex; + sint64 allowedBidderWalletSetIndex; + }; + + struct GetterScan_locals + { + AuctionData auction; + AuctionParticipantData participantData; + AuctionSummary auctionSummary; + ParticipantSummary participantSummary; + UserParticipationSummary userParticipationSummary; + SelectNextRetainedAuction_input selectNextAuctionInput; + SelectNextRetainedAuction_output selectNextAuctionOutput; + FindAuction_input findAuctionInput; + FindAuction_output findAuctionOutput; + uint64 auctionIndex; + uint64 boundedLimit; + uint64 metadataIndex; + uint64 requestedIndex; + uint64 participantSlotIndex; + uint64 historyIndex; + uint64 scannedAuctionCount; + sint64 auctionElementIndex; + uint8 metadataMatches; + }; + + using GetContractStats_locals = GetterScan_locals; + + struct GetAuctionSummaries_locals + { + AuctionData auction; + AuctionSummary auctionSummary; + SelectNextRetainedAuction_input selectNextAuctionInput; + SelectNextRetainedAuction_output selectNextAuctionOutput; + uint64 boundedLimit; + uint64 scannedAuctionCount; + }; + + struct GetActiveAuctionIndices_locals + { + SelectNextRetainedAuction_input selectNextAuctionInput; + SelectNextRetainedAuction_output selectNextAuctionOutput; + uint64 boundedLimit; + uint64 scannedAuctionCount; + }; + + struct GetAuctionsBySeller_locals + { + AuctionData auction; + AuctionSummary auctionSummary; + SelectNextRetainedAuction_input selectNextAuctionInput; + SelectNextRetainedAuction_output selectNextAuctionOutput; + CountRetainedAuctionsBySeller_input countAuctionsInput; + CountRetainedAuctionsBySeller_output countAuctionsOutput; + uint64 boundedLimit; + uint64 scannedAuctionCount; + }; + + struct GetAuctionByMetadataCid_locals + { + FindFirstRetainedAuctionByMetadataCid_input findAuctionInput; + FindFirstRetainedAuctionByMetadataCid_output findAuctionOutput; + }; + + using GetAuctionSummariesByIndexBatch_locals = GetterScan_locals; + using GetAuctionParticipants_locals = GetterScan_locals; + using GetUserParticipations_locals = GetterScan_locals; + + struct GetAuctionCountBySeller_locals + { + CountRetainedAuctionsBySeller_input countAuctionsInput; + CountRetainedAuctionsBySeller_output countAuctionsOutput; + }; + + using GetAuctionAtCreationSnapshot_locals = GetterScan_locals; + + struct GetAuctionParticipant_locals + { + AuctionParticipantData participantData; + uint64 participantSlotIndex; + uint64 bestParticipantSlotIndex; + uint8 bestParticipantFound; + }; + + struct GetClosedAuctionHistory_locals + { + AuctionData auction; + uint64 historyIndex; + }; + + /** @brief Internal input used to compute Batch Auction capacity at a candidate bid price. */ + struct ComputeBatchBidAvailability_input + { + /** @brief Monotonic index of the Batch Auction to inspect. */ + uint64 auctionIndex; + + /** @brief Candidate bid price; zero returns capacity at the computed minimum valid price. */ + uint64 bidAmount; + }; + + using ComputeBatchBidAvailability_output = GetBatchAuctionBidAvailability_output; + + struct ComputeBatchBidAvailability_locals + { + AuctionData auction; + AuctionParticipantData participantData; + uint64 lowestWinningPrice; + uint64 outputPrice; + uint64 priorityQuantity; + uint64 salePriorityQuantity; + uint64 effectiveCoverageQuantity; + uint64 participantIndex; + uint8 lowestWinningPriceFound; + }; + + struct GetBatchAuctionBidAvailability_locals + { + ComputeBatchBidAvailability_input computeBatchBidAvailabilityInput; + IsClosedAuctionRetained_input isClosedAuctionRetainedInput; + IsClosedAuctionRetained_output isClosedAuctionRetainedOutput; + }; + + /** @brief Internal input used to verify whether the invocator satisfies any private asset requirement. */ + struct HasRequiredAccessAsset_input + { + /** @brief Monotonic index of the auction whose private asset-based access rules should be evaluated. */ + uint64 auctionIndex; + }; + + /** @brief Internal output of the private asset access check. */ + struct HasRequiredAccessAsset_output + { + /** @brief Flag indicating whether the invocator owns the minimum quantity of any required asset. */ + uint8 hasRequiredAccessAsset; + }; + + struct HasRequiredAccessAsset_locals + { + AuctionData auction; + AuctionAssetEntry requiredAccessAsset; + sint64 requiredAccessAssetSetIndex; + sint64 possessedAccessShares; + }; + + /** @brief Internal input used to settle a batch auction after its bidding window closes. */ + struct FinalizeBatchAuction_input + { + /** @brief Timestamp used as the auction settlement time. */ + DateAndTime currentDate; + + /** @brief Monotonic index of the batch auction to finalize. */ + uint64 auctionIndex; + }; + + /** @brief Internal output returned after batch auction finalization. */ + struct FinalizeBatchAuction_output + { + /** + * @brief Flag indicating whether batch settlement finished successfully. + * @note Final allocations are never smaller than the auction minimum; any insufficient remainder is returned to the seller. + */ + uint8 success; + }; + + /** @brief Internal input used to settle a standard auction when it is accepted or auto-finalized. */ + struct FinalizeStandardAuction_input + { + /** @brief Timestamp used as the auction settlement time. */ + DateAndTime currentDate; + + /** @brief Monotonic index of the standard auction to finalize. */ + uint64 auctionIndex; + }; + + /** @brief Internal output returned after standard auction finalization. */ + struct FinalizeStandardAuction_output + { + /** @brief Flag indicating whether standard auction settlement finished successfully. */ + uint8 success; + }; + + /** @brief Internal input used to reject a pending standard auction during the seller decision window. */ + struct RejectStandardAuction_input + { + /** @brief Timestamp used as the auction settlement time. */ + DateAndTime currentDate; + + /** @brief Monotonic index of the pending standard auction to reject. */ + uint64 auctionIndex; + }; + + /** @brief Internal output returned after rejecting a pending standard auction. */ + struct RejectStandardAuction_output + { + /** @brief Amount refunded to the highest bidder after the rejection. */ + uint64 refundedAmount; + + /** @brief Flag indicating whether the rejection flow finished successfully. */ + uint8 success; + }; + + /** @brief Internal input used to evaluate whether auction interactions are currently paused. */ + struct IsAuctionInteractionPaused_input + { + }; + + /** @brief Internal output of the auction interaction pause check. */ + struct IsAuctionInteractionPaused_output + { + /** @brief Flag indicating whether auction interactions are blocked by bootstrap time or by epoch timing pauses. */ + uint8 isPaused; + }; + + /** @brief Internal input used to resolve the currently active global auction pause interval. */ + struct GetAuctionPauseState_input + { + }; + + /** @brief Internal output describing the current global auction pause interval. */ + struct GetAuctionPauseState_output + { + /** @brief Pause start timestamp for the current active pause interval. */ + DateAndTime pauseStartedAt; + + /** @brief Pause end timestamp for the current active pause interval. */ + DateAndTime pauseEndsAt; + + /** @brief Flag indicating whether auction timers are currently paused. */ + uint8 isPaused; + }; + + /** @brief Internal locals used to resolve the currently active global auction pause interval. */ + struct GetAuctionPauseState_locals + { + /** @brief Compact current date marker used to detect the bootstrap default time sentinel. */ + DateAndTime currentDate; + uint32 currentDateStamp; + }; + + using SyncAuctionPauseState_input = NoData; + using SyncAuctionPauseState_output = NoData; + + /** @brief Internal locals used to synchronize auction deadlines with the global pause interval. */ + struct SyncAuctionPauseState_locals + { + AuctionData auction; + DateAndTime currentDate; + GetAuctionPauseState_input getAuctionPauseStateInput; + GetAuctionPauseState_output getAuctionPauseStateOutput; + uint64 pausedSeconds; + sint64 auctionIndex; + }; + + /** @brief Internal input used to split auction proceeds between seller and configured fee recipients. */ + struct DistributeAuctionRevenue_input + { + /** @brief Seller wallet that receives the net proceeds. */ + id seller; + + /** @brief Gross amount collected from the auction before fee distribution. */ + uint64 grossAmount; + }; + + /** @brief Internal output returned after auction revenue distribution is computed. */ + struct DistributeAuctionRevenue_output + { + /** @brief Net amount that should be transferred to the seller after auction fees. */ + uint64 sellerPayout; + + /** @brief Flag indicating whether the revenue distribution completed successfully. */ + uint8 success; + }; + + /** @brief Internal input used to accrue a service fee in the shared Nostromo fee pool. */ + struct AccumulateAuctionServiceFee_input + { + /** @brief Fee amount that should be accumulated. */ + uint64 feeAmount; + }; + + /** @brief Internal output returned after service-fee accrual is completed. */ + struct AccumulateAuctionServiceFee_output + { + /** @brief Flag indicating whether the service fee was recorded. */ + uint8 success; + }; + + using DistributeNostromoFeePool_input = NoData; + + struct DistributeNostromoFeePool_output + { + /** @brief Flag indicating whether every current pool accumulator was durably settled. */ + uint8 success; + }; + + /** @brief Internal input used to compute the remaining post-BEGIN_EPOCH launch pause. */ + struct GetTicksBeforeAuctionLaunchInternal_input + { + }; + + /** @brief Internal output containing the remaining post-BEGIN_EPOCH launch pause. */ + struct GetTicksBeforeAuctionLaunchInternal_output + { + /** @brief Number of ticks remaining before auction interactions resume after `BEGIN_EPOCH`. */ + uint32 ticks; + }; + + struct GetTicksBeforeAuctionLaunchInternal_locals + { + DateAndTime currentDate; + DateAndTime pauseEndsAt; + uint64 remainingSeconds; + }; + + struct GetTicksBeforeAuctionLaunch_locals + { + DateAndTime currentDate; + DateAndTime pauseEndsAt; + uint64 remainingSeconds; + }; + + /** @brief Internal input used to register a QU liability before settlement side effects are committed. */ + struct QueueQuPayout_input + { + id recipient; + uint64 amount; + }; + + struct QueueQuPayout_output + { + uint8 success; + }; + + struct QueueQuPayout_locals + { + uint64 previousAmount; + uint64 updatedAmount; + sint64 payoutIndex; + }; + + /** @brief Internal input used to discharge a bounded number of QPI-sized payout chunks. */ + struct FlushQuPayout_input + { + id recipient; + uint64 maxChunks; + }; + + struct FlushQuPayout_output + { + uint64 transferredAmount; + uint64 remainingAmount; + uint8 success; + }; + + struct FlushQuPayout_locals + { + uint64 chunkAmount; + uint64 chunkIndex; + sint64 transferResult; + }; + + using ProcessPendingQuPayouts_input = NoData; + using ProcessPendingQuPayouts_output = NoData; + + /** @brief Internal locals used by the bounded round-robin pending-payout processor. */ + struct ProcessPendingQuPayouts_locals + { + FlushQuPayout_input flushQuPayoutInput; + FlushQuPayout_output flushQuPayoutOutput; + id pendingPayoutRecipient; + uint64 payoutScanIndex; + uint64 payoutTargetRecipientCount; + uint64 processedPayoutRecipientCount; + sint64 payoutElementIndex; + }; + + struct QueueAndFlushQuPayout_input + { + id recipient; + uint64 amount; + uint64 maxChunks; + }; + + struct QueueAndFlushQuPayout_output + { + uint64 transferredAmount; + uint64 remainingAmount; + uint8 success; + }; + + struct QueueAndFlushQuPayout_locals + { + QueueQuPayout_input queueQuPayoutInput; + QueueQuPayout_output queueQuPayoutOutput; + FlushQuPayout_input flushQuPayoutInput; + FlushQuPayout_output flushQuPayoutOutput; + }; + + /** @brief Internal input used to move a bid record from live storage into bounded history. */ + struct ArchiveParticipant_input + { + AuctionParticipantData participantData; + }; + + using ArchiveParticipant_output = NoData; + + struct ArchiveParticipant_locals + { + uint64 historyIndex; + }; + + /** @brief Internal input used to process a batch auction bid after the common PlaceBid checks succeed. */ + struct ProcessBatchBid_input + { + /** @brief Monotonic index of the target batch auction. */ + uint64 auctionIndex; + + /** @brief Quantity requested by the bidder in the batch auction. */ + uint64 effectiveQuantity; + + /** @brief Offered price per asset for the requested quantity in the batch auction. */ + uint64 bidAmount; + + /** @brief Timestamp of the accepted bid. */ + DateAndTime currentDate; + + /** @brief Seconds elapsed since auction creation at the moment of the bid. */ + uint64 elapsedSeconds; + }; + + /** @brief Internal input used to refresh the cached highest bid fields of one batch auction. */ + struct RecomputeBatchHighestBid_input + { + /** @brief Monotonic index of the batch auction whose cached top bid must be rebuilt. */ + uint64 auctionIndex; + }; + + using RecomputeBatchHighestBid_output = NoData; + + /** @brief Internal output returned after processing a batch auction bid. */ + struct ProcessBatchBid_output + { + /** @brief Amount that remains escrowed for the accepted batch bid. */ + uint64 escrowedAmount; + + /** @brief Amount refunded during batch bid processing. */ + uint64 refundedAmount; + + /** @brief Result code describing whether the batch bid processing succeeded. */ + EAuctionError errorCode; + + /** @brief Flag indicating whether batch bid processing completed successfully. */ + uint8 success; + }; + + struct ProcessBatchBid_locals + { + AuctionData auction; + AuctionParticipantData participantData; + AuctionParticipantData worstParticipantData; + ComputeBatchBidAvailability_input computeBatchBidAvailabilityInput; + ComputeBatchBidAvailability_output computeBatchBidAvailabilityOutput; + RecomputeBatchHighestBid_input recomputeBatchHighestBidInput; + RecomputeBatchHighestBid_output recomputeBatchHighestBidOutput; + ArchiveParticipant_input archiveParticipantInput; + ArchiveParticipant_output archiveParticipantOutput; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; + AccumulateAuctionServiceFee_input accumulateAuctionServiceFeeInput; + AccumulateAuctionServiceFee_output accumulateAuctionServiceFeeOutput; + uint64 activeQuantity; + uint64 displacedQuantity; + uint64 displacedRefund; + uint64 excessQuantity; + uint64 remainingWorstQuantity; + CalculateBatchAuctionBidFee_output bidFeeCalculation; + uint64 participantIndex; + uint64 freeParticipantSlotIndex; + uint64 worstParticipantSlotIndex; + uint8 worstParticipantFound; + uint8 freeParticipantSlotFound; + }; + + struct RecomputeBatchHighestBid_locals + { + AuctionData auction; + AuctionParticipantData participantData; + AuctionParticipantData bestParticipantData; + uint64 participantIndex; + uint64 bestParticipantSlotIndex; + uint8 bestParticipantFound; + }; + + /** @brief Internal input used to process a standard auction bid after the common PlaceBid checks succeed. */ + struct ProcessStandardBid_input + { + /** @brief Monotonic index of the target standard auction. */ + uint64 auctionIndex; + + /** @brief Total amount the bidder commits for the standard auction lot. */ + uint64 bidAmount; + + /** @brief Timestamp of the accepted bid. */ + DateAndTime currentDate; + + /** @brief Seconds elapsed since auction creation at the moment of the bid. */ + uint64 elapsedSeconds; + }; + + /** @brief Internal output returned after processing a standard auction bid. */ + struct ProcessStandardBid_output + { + /** @brief Amount that remains escrowed for the accepted standard bid. */ + uint64 escrowedAmount; + + /** @brief Amount refunded during standard bid processing. */ + uint64 refundedAmount; + + /** @brief Result code describing whether the standard bid processing succeeded. */ + EAuctionError errorCode; + + /** @brief Flag indicating whether standard bid processing completed successfully. */ + uint8 success; + }; + + struct ProcessStandardBid_locals + { + AuctionData auction; + AuctionParticipantData participantData; + AuctionParticipantData previousHighestBidderData; + FinalizeStandardAuction_input finalizeStandardAuctionInput; + FinalizeStandardAuction_output finalizeStandardAuctionOutput; + ArchiveParticipant_input archiveParticipantInput; + ArchiveParticipant_output archiveParticipantOutput; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; + uint64 previousEscrow; + uint64 requiredEscrow; + uint64 participantSlotIndex; + uint64 highestBidderSlotIndex; + uint64 freeParticipantSlotIndex; + uint8 participantExists; + uint8 highestBidderExists; + uint8 freeParticipantSlotFound; + uint8 finalizeImmediately; + }; + + /** @brief Internal input used to validate the IPFS metadata CID format required by the Auction House. */ + struct ValidateMetadataCid_input + { + /** @brief Candidate lowercase base32 CIDv1 for auction metadata stored in Pinata. */ + Array metadataIpfsCid; + }; + + /** @brief Internal output of the metadata CID validation routine. */ + struct ValidateMetadataCid_output + { + /** @brief Flag indicating whether the metadata CID has the required lowercase base32 CIDv1 format. */ + uint8 isValid; + }; + + struct ValidateMetadataCid_locals + { + uint64 cidIndex; + uint8 cidChar; + uint8 hasPayloadCharacters; + uint8 reachedTerminator; + }; + + /** @brief Internal input used to verify that the seller owns enough shares for every asset in the auction lot. */ + struct VerifyAuctionLotBalances_input + { + /** @brief Auction lot that should be checked against the seller balance. */ + Array auctionLotItems; + }; + + /** @brief Internal output of the seller balance verification routine. */ + struct VerifyAuctionLotBalances_output + { + /** @brief Flag indicating whether the seller owns enough shares for the entire lot. */ + uint8 hasEnoughBalance; + }; + + struct VerifyAuctionLotBalances_locals + { + AuctionAssetEntry lotItem; + uint64 lotItemIndex; + sint64 possessedShares; + }; + + /** @brief Internal input used to transfer the auction lot from the seller into contract escrow. */ + struct EscrowAuctionLotAssets_input + { + /** @brief Auction lot that must be moved into contract escrow. */ + Array auctionLotItems; + }; + + /** @brief Internal output of the lot escrow routine. */ + struct EscrowAuctionLotAssets_output + { + /** @brief Flag indicating whether every lot asset was successfully escrowed. */ + uint8 success; + }; + + struct EscrowAuctionLotAssets_locals + { + AuctionAssetEntry lotItem; + uint64 lotItemIndex; + uint64 rollbackLotItemIndex; + sint64 remainingShares; + }; + + /** @brief Internal input used to return an auction lot from contract escrow to a target wallet. */ + struct RollbackAuctionLotAssets_input + { + /** @brief Auction lot that must be transferred out of contract escrow. */ + Array auctionLotItems; + + /** @brief Destination wallet that should receive the lot from escrow. */ + id recipient; + }; + + using RollbackAuctionLotAssets_output = NoData; + + struct RollbackAuctionLotAssets_locals + { + AuctionAssetEntry lotItem; + uint64 lotItemIndex; + }; + + /** @brief Internal input used to archive and remove a closed auction from active storage. */ + struct ArchiveClosedAuction_input + { + AuctionData auction; + }; + + using ArchiveClosedAuction_output = NoData; + + struct ArchiveClosedAuction_locals + { + uint64 historyIndex; + }; + + struct FinalizeBatchAuction_locals + { + AuctionData auction; + AuctionParticipantData participantData; + AuctionParticipantData bestParticipantData; + AuctionAssetEntry batchLotItem; + DistributeAuctionRevenue_input distributeAuctionRevenueInput; + DistributeAuctionRevenue_output distributeAuctionRevenueOutput; + ArchiveParticipant_input archiveParticipantInput; + ArchiveParticipant_output archiveParticipantOutput; + ArchiveClosedAuction_input archiveClosedAuctionInput; + ArchiveClosedAuction_output archiveClosedAuctionOutput; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; + DateAndTime currentDate; + uint64 remainingQuantity; + uint64 allocatedQuantity; + uint64 requiredPayment; + uint64 refundAmount; + uint64 soldQuantity; + uint64 totalGrossAmount; + uint64 lotItemIndex; + uint64 participantIndex; + uint64 bestParticipantSlotIndex; + uint8 bestParticipantFound; + uint8 lotItemFound; + }; + + struct FinalizeStandardAuction_locals + { + AuctionData auction; + AuctionParticipantData highestBidderData; + RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; + RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; + DistributeAuctionRevenue_input distributeAuctionRevenueInput; + DistributeAuctionRevenue_output distributeAuctionRevenueOutput; + ArchiveParticipant_input archiveParticipantInput; + ArchiveParticipant_output archiveParticipantOutput; + ArchiveClosedAuction_input archiveClosedAuctionInput; + ArchiveClosedAuction_output archiveClosedAuctionOutput; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; + uint64 highestBidderSlotIndex; + uint8 highestBidderExists; + uint8 lotSold; + }; + + struct DistributeAuctionRevenue_locals + { + AuctionRevenueBreakdown auctionRevenueBreakdown; + NostromoFeePool feePool; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; + uint64 shareholderFeeTierIndex; + }; + + struct AccumulateAuctionServiceFee_locals + { + NostromoFeePool feePool; + }; + + struct DistributeNostromoFeePool_locals + { + AuctionServiceFeeBreakdown auctionServiceFeeBreakdown; + NostromoFeePool feePool; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; + uint64 shareholderDividendAmount; + uint64 distributedDividendAmount; + uint64 dividendPerShare; + }; + + struct RejectStandardAuction_locals + { + AuctionData auction; + AuctionParticipantData highestBidderData; + RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; + RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; + ArchiveParticipant_input archiveParticipantInput; + ArchiveParticipant_output archiveParticipantOutput; + ArchiveClosedAuction_input archiveClosedAuctionInput; + ArchiveClosedAuction_output archiveClosedAuctionOutput; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; + uint64 highestBidderSlotIndex; + uint8 highestBidderExists; + }; + + struct CreateAuction_locals + { + AuctionData auction; + NostromoProcedureLog log; + IsAuctionInteractionPaused_input isAuctionInteractionPausedInput; + IsAuctionInteractionPaused_output isAuctionInteractionPausedOutput; + ValidateMetadataCid_input validateMetadataCidInput; + ValidateMetadataCid_output validateMetadataCidOutput; + AnalyzeAuctionLot_input analyzeAuctionLotInput; + AnalyzeAuctionLot_output analyzeAuctionLotOutput; + CountAllowedBidderWallets_input countAllowedBidderWalletsInput; + CountAllowedBidderWallets_output countAllowedBidderWalletsOutput; + CountRequiredAccessAssets_input countRequiredAccessAssetsInput; + CountRequiredAccessAssets_output countRequiredAccessAssetsOutput; + AuctionAssetEntry requiredAccessAsset; + VerifyAuctionLotBalances_input verifyAuctionLotBalancesInput; + EscrowAuctionLotAssets_input escrowAuctionLotAssetsInput; + RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; + AccumulateAuctionServiceFee_input accumulateAuctionServiceFeeInput; + sint64 requiredFee; + sint64 existingRequiredAccessQuantity; + uint64 resolvedQuantityForSale; + uint64 resolvedMinimumPurchaseQuantity; + uint64 allowedWalletIndex; + uint64 requiredAccessAssetIndex; + RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; + EscrowAuctionLotAssets_output escrowAuctionLotAssetsOutput; + VerifyAuctionLotBalances_output verifyAuctionLotBalancesOutput; + AccumulateAuctionServiceFee_output accumulateAuctionServiceFeeOutput; + }; + + struct PlaceBid_locals + { + AuctionData auction; + FindAuction_input findAuctionInput; + FindAuction_output findAuctionOutput; + NostromoProcedureLog log; + IsAuctionInteractionPaused_input isAuctionInteractionPausedInput; + IsAuctionInteractionPaused_output isAuctionInteractionPausedOutput; + HasRequiredAccessAsset_input hasRequiredAccessAssetInput; + HasRequiredAccessAsset_output hasRequiredAccessAssetOutput; + ProcessBatchBid_input processBatchBidInput; + ProcessBatchBid_output processBatchBidOutput; + ProcessStandardBid_input processStandardBidInput; + ProcessStandardBid_output processStandardBidOutput; + uint64 elapsedSeconds; + DateAndTime currentDate; + uint8 hasAccess; + }; + + struct CancelAuction_locals + { + AuctionData auction; + FindAuction_input findAuctionInput; + FindAuction_output findAuctionOutput; + AuctionParticipantData participantData; + NostromoProcedureLog log; + RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; + RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; + AccumulateAuctionServiceFee_input accumulateAuctionServiceFeeInput; + AccumulateAuctionServiceFee_output accumulateAuctionServiceFeeOutput; + ArchiveClosedAuction_input archiveClosedAuctionInput; + ArchiveClosedAuction_output archiveClosedAuctionOutput; + DateAndTime currentDate; + uint64 cancellationBaseAmount; + uint64 participantIndex; + }; + + struct ResolvePendingStandardAuction_locals + { + AuctionData auction; + FindAuction_input findAuctionInput; + FindAuction_output findAuctionOutput; + DateAndTime currentDate; + NostromoProcedureLog log; + + IsAuctionInteractionPaused_input isAuctionInteractionPausedInput; + IsAuctionInteractionPaused_output isAuctionInteractionPausedOutput; + FinalizeStandardAuction_input finalizeStandardAuctionInput; + FinalizeStandardAuction_output finalizeStandardAuctionOutput; + RejectStandardAuction_input rejectStandardAuctionInput; + RejectStandardAuction_output rejectStandardAuctionOutput; + }; + + struct END_TICK_locals + { + AuctionData auction; + DateAndTime currentDate; + SyncAuctionPauseState_input syncAuctionPauseStateInput; + SyncAuctionPauseState_output syncAuctionPauseStateOutput; + uint64 elapsedSeconds; + uint32 currentDateStamp; + sint64 auctionIndex; + FinalizeBatchAuction_input finalizeBatchAuctionInput; + FinalizeBatchAuction_output finalizeBatchAuctionOutput; + FinalizeStandardAuction_input finalizeStandardAuctionInput; + FinalizeStandardAuction_output finalizeStandardAuctionOutput; + sint64 currentReserve; + sint64 reserveDrop; + uint64 guardElapsedSeconds; + uint64 guardDropThreshold; + }; + + struct BEGIN_EPOCH_locals + { + QX::Fees_input feesInput; + QX::Fees_output feesOutput; + }; + + struct END_EPOCH_locals + { + DistributeNostromoFeePool_input distributeNostromoFeePoolInput; + DistributeNostromoFeePool_output distributeNostromoFeePoolOutput; + ProcessPendingQuPayouts_input processPendingQuPayoutsInput; + ProcessPendingQuPayouts_output processPendingQuPayoutsOutput; + }; + + /** @brief Input payload used to move share management rights to another managing contract. */ + struct TransferShareManagementRights_input + { + /** @brief Asset whose management rights should be transferred. */ + Asset asset; + + /** @brief Number of shares whose management rights should be transferred. */ + sint64 numberOfShares; + + /** @brief Destination managing contract index. */ + uint32 newManagingContractIndex; + }; + + /** @brief Result of a share management rights transfer request. */ + struct TransferShareManagementRights_output + { + /** @brief Number of shares whose management rights were transferred. */ + sint64 transferredNumberOfShares; + + /** @brief Result code describing whether the transfer request succeeded. */ + EAuctionError errorCode; + }; + + struct TransferShareManagementRights_locals + { + NostromoProcedureLog log; + + sint64 result; + sint64 reward; + sint64 refundAmount; + bit success; + }; + + struct SetAuctionFees_locals + { + NostromoProcedureLog log; + }; + + struct SetAuctionFeesByManagement_locals + { + NostromoProcedureLog log; + }; + + struct SetManagement_locals + { + NostromoProcedureLog log; + }; + + struct SetFeeReserveGuardConfig_locals + { + NostromoProcedureLog log; + }; + + struct SetEmergencyPause_locals + { + NostromoProcedureLog log; + }; + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_PROCEDURE(CreateAuction, static_cast(EProcedureId::CreateAuction)); + REGISTER_USER_PROCEDURE(PlaceBid, static_cast(EProcedureId::PlaceBid)); + REGISTER_USER_PROCEDURE(CancelAuction, static_cast(EProcedureId::CancelAuction)); + REGISTER_USER_PROCEDURE(TransferShareManagementRights, static_cast(EProcedureId::TransferShareManagementRights)); + REGISTER_USER_PROCEDURE(ResolvePendingStandardAuction, static_cast(EProcedureId::ResolvePendingStandardAuction)); + REGISTER_USER_PROCEDURE(SetAuctionFees, static_cast(EProcedureId::SetAuctionFees)); + REGISTER_USER_PROCEDURE(SetAuctionFeesByManagement, static_cast(EProcedureId::SetAuctionFeesByManagement)); + REGISTER_USER_PROCEDURE(SetManagement, static_cast(EProcedureId::SetManagement)); + REGISTER_USER_PROCEDURE(SetFeeReserveGuardConfig, static_cast(EProcedureId::SetFeeReserveGuardConfig)); + REGISTER_USER_PROCEDURE(SetEmergencyPause, static_cast(EProcedureId::SetEmergencyPause)); + + REGISTER_USER_FUNCTION(GetAuctionByIndex, static_cast(EFunctionId::GetAuctionByIndex)); + REGISTER_USER_FUNCTION(GetAuctionParticipant, static_cast(EFunctionId::GetAuctionParticipant)); + REGISTER_USER_FUNCTION(GetTicksBeforeAuctionLaunch, static_cast(EFunctionId::GetTicksBeforeAuctionLaunch)); + REGISTER_USER_FUNCTION(GetAuctionFees, static_cast(EFunctionId::GetAuctionFees)); + REGISTER_USER_FUNCTION(GetFeeRecipients, static_cast(EFunctionId::GetFeeRecipients)); + REGISTER_USER_FUNCTION(GetClosedAuctionHistory, static_cast(EFunctionId::GetClosedAuctionHistory)); + REGISTER_USER_FUNCTION(GetRouteAllFeesToDevelopment, static_cast(EFunctionId::GetRouteAllFeesToDevelopment)); + REGISTER_USER_FUNCTION(GetContractStats, static_cast(EFunctionId::GetContractStats)); + REGISTER_USER_FUNCTION(GetAuctionSummaries, static_cast(EFunctionId::GetAuctionSummaries)); + REGISTER_USER_FUNCTION(GetActiveAuctionIndices, static_cast(EFunctionId::GetActiveAuctionIndices)); + REGISTER_USER_FUNCTION(GetAuctionsBySeller, static_cast(EFunctionId::GetAuctionsBySeller)); + REGISTER_USER_FUNCTION(GetAuctionByMetadataCid, static_cast(EFunctionId::GetAuctionByMetadataCid)); + REGISTER_USER_FUNCTION(GetAuctionSummariesByIndexBatch, static_cast(EFunctionId::GetAuctionSummariesByIndexBatch)); + REGISTER_USER_FUNCTION(GetAuctionParticipants, static_cast(EFunctionId::GetAuctionParticipants)); + REGISTER_USER_FUNCTION(GetUserParticipations, static_cast(EFunctionId::GetUserParticipations)); + REGISTER_USER_FUNCTION(GetLatestAuctionIndex, static_cast(EFunctionId::GetLatestAuctionIndex)); + REGISTER_USER_FUNCTION(GetAuctionCountBySeller, static_cast(EFunctionId::GetAuctionCountBySeller)); + REGISTER_USER_FUNCTION(GetAuctionAtCreationSnapshot, static_cast(EFunctionId::GetAuctionAtCreationSnapshot)); + REGISTER_USER_FUNCTION(GetBatchAuctionBidAvailability, static_cast(EFunctionId::GetBatchAuctionBidAvailability)); + REGISTER_USER_FUNCTION(CalculateBatchAuctionBidFee, static_cast(EFunctionId::CalculateBatchAuctionBidFee)); + REGISTER_USER_FUNCTION(GetPendingServiceFeePool, static_cast(EFunctionId::GetPendingServiceFeePool)); + REGISTER_USER_FUNCTION(GetFeeReserveGuardState, static_cast(EFunctionId::GetFeeReserveGuardState)); + REGISTER_USER_FUNCTION(GetPendingPayout, static_cast(EFunctionId::GetPendingPayout)); + REGISTER_USER_FUNCTION(GetNostromoFeePool, static_cast(EFunctionId::GetNostromoFeePool)); + } + + /** + * @brief Initializes default governance, fee, pause, and guard settings. + */ + INITIALIZE() + { + // Install the default governance, fee, pause, and guard configuration into the zeroed contract state. + state.mut().privateAuctionFee = NOST_DEFAULT_PRIVATE_AUCTION_FEE; + state.mut().publicAuctionCreationFee = NOST_PUBLIC_AUCTION_CREATION_FEE; + state.mut().auctionCancellationFeeBasisPoints = NOST_DEFAULT_AUCTION_CANCELLATION_FEE_BP; + state.mut().managementFeeBasisPoints = NOST_DEFAULT_AUCTION_MANAGEMENT_FEE_BP; + state.mut().developmentFeeBasisPoints = NOST_DEFAULT_AUCTION_DEVELOPMENT_FEE_BP; + state.mut().takeoverCoordinatorFeeBasisPoints = NOST_DEFAULT_AUCTION_TAKEOVER_COORDINATOR_FEE_BP; + state.mut().shareholderDividendBasisPoints = NOST_DEFAULT_AUCTION_SHAREHOLDER_DIVIDEND_BP; + state.mut().shareholderFeeBasisPointsTier1 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_1; + state.mut().shareholderFeeBasisPointsTier2 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_2; + state.mut().shareholderFeeBasisPointsTier3 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_3; + state.mut().shareholderFeeBasisPointsTier4 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4; + state.mut().maxAuctionDurationDays = NOST_AUCTION_MAX_DURATION_DAYS; + state.mut().isAuctionTimerPaused = 1; + state.mut().routeAllFeesToDevelopment = NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT; + state.mut().auctionTimerPauseStartedAt.setInvalid(); + state.mut().auctionTimerPauseEndsAt.setInvalid(); + state.mut().feeReserveGuardDropBasisPoints = NOST_DEFAULT_FEE_RESERVE_GUARD_DROP_BP; + state.mut().feeReserveGuardWindowSeconds = NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS; + state.mut().management = ID(_I, _G, _P, _Z, _X, _Q, _O, _R, _J, _Y, _Q, _P, _A, _G, _V, _A, _B, _N, _T, _N, _I, _S, _O, _Y, _T, _M, _T, _A, + _N, _M, _K, _Z, _A, _S, _T, _P, _P, _G, _Z, _O, _N, _A, _Q, _J, _X, _Q, _O, _S, _W, _Q, _O, _V, _J, _C, _K, _D); + state.mut().development = ID(_D, _Q, _V, _H, _M, _Z, _F, _C, _W, _O, _K, _M, _H, _F, _B, _H, _L, _X, _U, _I, _U, _G, _P, _P, _X, _R, _Z, _C, + _U, _V, _S, _N, _J, _F, _Z, _J, _F, _M, _Q, _M, _Y, _D, _B, _X, _E, _S, _E, _A, _T, _M, _W, _L, _K, _N, _L, _D); + state.mut().takeoverCoordinator = + ID(_X, _J, _O, _S, _N, _L, _T, _Z, _V, _V, _H, _N, _Z, _C, _B, _Y, _X, _I, _E, _V, _N, _E, _P, _P, _B, _O, _Q, _A, _W, _D, _B, _V, _G, _E, + _N, _Z, _O, _X, _S, _V, _O, _B, _K, _G, _Z, _C, _C, _F, _D, _B, _D, _M, _T, _M, _L, _C); + } + + MIGRATE() + { + state.mut().privateAuctionFee = NOST_DEFAULT_PRIVATE_AUCTION_FEE; + state.mut().publicAuctionCreationFee = NOST_PUBLIC_AUCTION_CREATION_FEE; + state.mut().auctionCancellationFeeBasisPoints = NOST_DEFAULT_AUCTION_CANCELLATION_FEE_BP; + state.mut().managementFeeBasisPoints = NOST_DEFAULT_AUCTION_MANAGEMENT_FEE_BP; + state.mut().developmentFeeBasisPoints = NOST_DEFAULT_AUCTION_DEVELOPMENT_FEE_BP; + state.mut().takeoverCoordinatorFeeBasisPoints = NOST_DEFAULT_AUCTION_TAKEOVER_COORDINATOR_FEE_BP; + state.mut().shareholderDividendBasisPoints = NOST_DEFAULT_AUCTION_SHAREHOLDER_DIVIDEND_BP; + state.mut().shareholderFeeBasisPointsTier1 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_1; + state.mut().shareholderFeeBasisPointsTier2 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_2; + state.mut().shareholderFeeBasisPointsTier3 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_3; + state.mut().shareholderFeeBasisPointsTier4 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4; + state.mut().maxAuctionDurationDays = NOST_AUCTION_MAX_DURATION_DAYS; + state.mut().routeAllFeesToDevelopment = NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT; + state.mut().feeReserveGuardDropBasisPoints = NOST_DEFAULT_FEE_RESERVE_GUARD_DROP_BP; + state.mut().feeReserveGuardWindowSeconds = NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS; + state.mut().management = ID(_I, _G, _P, _Z, _X, _Q, _O, _R, _J, _Y, _Q, _P, _A, _G, _V, _A, _B, _N, _T, _N, _I, _S, _O, _Y, _T, _M, _T, _A, + _N, _M, _K, _Z, _A, _S, _T, _P, _P, _G, _Z, _O, _N, _A, _Q, _J, _X, _Q, _O, _S, _W, _Q, _O, _V, _J, _C, _K, _D); + state.mut().development = ID(_D, _Q, _V, _H, _M, _Z, _F, _C, _W, _O, _K, _M, _H, _F, _B, _H, _L, _X, _U, _I, _U, _G, _P, _P, _X, _R, _Z, _C, + _U, _V, _S, _N, _J, _F, _Z, _J, _F, _M, _Q, _M, _Y, _D, _B, _X, _E, _S, _E, _A, _T, _M, _W, _L, _K, _N, _L, _D); + state.mut().takeoverCoordinator = + ID(_X, _J, _O, _S, _N, _L, _T, _Z, _V, _V, _H, _N, _Z, _C, _B, _Y, _X, _I, _E, _V, _N, _E, _P, _P, _B, _O, _Q, _A, _W, _D, _B, _V, _G, _E, + _N, _Z, _O, _X, _S, _V, _O, _B, _K, _G, _Z, _C, _C, _F, _D, _B, _D, _M, _T, _M, _L, _C); + } + + /** + * @brief Allows share acquisition without charging an additional contract fee. + */ + PRE_ACQUIRE_SHARES() + { + output.requestedFee = 0; + output.allowTransfer = true; + } + + /** + * @brief Refreshes epoch-scoped configuration and arms auction timer pauses. + */ + BEGIN_EPOCH_WITH_LOCALS() + { + // Refresh the QX fee cache once per epoch so share transfers can expose current cost guidance. + CALL_OTHER_CONTRACT_FUNCTION(QX, Fees, locals.feesInput, locals.feesOutput); + // Preserve the previous cache when QX is temporarily unavailable; a failed call must not install an undefined fee. + if (interContractCallError == NoCallError) + { + state.mut().qxTransferFee = locals.feesOutput.transferFee; + } + + // Freeze auction timers across the epoch boundary; END_TICK later accounts this pause back into deadlines. + state.mut().isPostBeginEpochPauseArmed = 1; + if (!state.get().isAuctionTimerPaused) + { + state.mut().isAuctionTimerPaused = 1; + state.mut().auctionTimerPauseStartedAt = qpi.now(); + state.mut().auctionTimerPauseEndsAt = qpi.now(); + return; + } + + if (!state.get().auctionTimerPauseStartedAt.isValid() || qpi.now() < state.get().auctionTimerPauseStartedAt) + { + state.mut().auctionTimerPauseStartedAt = qpi.now(); + } + if (!state.get().auctionTimerPauseEndsAt.isValid() || qpi.now() > state.get().auctionTimerPauseEndsAt) + { + state.mut().auctionTimerPauseEndsAt = qpi.now(); + } + } + + /** + * @brief Retries pending QU payouts, settles the shared Nostromo fee pool, and performs storage cleanup. + */ + END_EPOCH_WITH_LOCALS() + { + CALL(ProcessPendingQuPayouts, locals.processPendingQuPayoutsInput, locals.processPendingQuPayoutsOutput); + + CALL(DistributeNostromoFeePool, locals.distributeNostromoFeePoolInput, locals.distributeNostromoFeePoolOutput); + + state.mut().auctionList.cleanupIfNeeded(); + state.mut().pendingQuPayouts.cleanupIfNeeded(); + } + + /** + * @brief Advances auction lifecycle state and finalizes auctions whose deadlines elapsed. + */ + END_TICK_WITH_LOCALS() + { + makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentDateStamp); + locals.currentDate = qpi.now(); + + // The reserve guard converts a sudden execution-fee reserve drop into an emergency pause. + if (!state.get().isEmergencyPaused) + { + locals.currentReserve = qpi.queryFeeReserve(SELF_INDEX); + // The first observation establishes a baseline instead of interpreting startup state as a reserve drop. + if (!state.get().feeReserveBaselineAt.isValid()) + { + state.mut().feeReserveBaseline = locals.currentReserve; + state.mut().feeReserveBaselineAt = locals.currentDate; + } + else + { + // Subsequent observations either trigger the guard or roll the baseline into a new window. + diffDateInSecond(state.get().feeReserveBaselineAt, locals.currentDate, locals.guardElapsedSeconds); + locals.reserveDrop = state.get().feeReserveBaseline - locals.currentReserve; + if (state.get().feeReserveBaseline > 0 && locals.reserveDrop > 0) + { + locals.guardDropThreshold = + div(smul(static_cast(state.get().feeReserveBaseline), state.get().feeReserveGuardDropBasisPoints), + NOST_BASIS_POINTS_SCALE); + if (static_cast(locals.reserveDrop) >= locals.guardDropThreshold && + locals.guardElapsedSeconds <= state.get().feeReserveGuardWindowSeconds) + { + state.mut().isEmergencyPaused = 1; + state.mut().emergencyPausedAt = locals.currentDate; + state.mut().feeReserveBaselineAt.setInvalid(); + } + else if (locals.guardElapsedSeconds >= state.get().feeReserveGuardWindowSeconds) + { + state.mut().feeReserveBaseline = locals.currentReserve; + state.mut().feeReserveBaselineAt = locals.currentDate; + } + } + else if (locals.guardElapsedSeconds >= state.get().feeReserveGuardWindowSeconds) + { + state.mut().feeReserveBaseline = locals.currentReserve; + state.mut().feeReserveBaselineAt = locals.currentDate; + } + } + } + + CALL(SyncAuctionPauseState, locals.syncAuctionPauseStateInput, locals.syncAuctionPauseStateOutput); + // Lifecycle transitions must not advance while SyncAuctionPauseState still owns the global timer freeze. + if (state.get().isAuctionTimerPaused) + { + return; + } + + // Only live auctions advance after pause synchronization has extended their timers. + locals.auctionIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); + while (locals.auctionIndex != NULL_INDEX) + { + locals.auction = state.get().auctionList.value(locals.auctionIndex); + switch (locals.auction.core.status) + { + case EAuctionStatus::Active: + diffDateInSecond(locals.auction.core.createdAt, locals.currentDate, locals.elapsedSeconds); + // Only an elapsed active auction is eligible for automatic settlement or seller-decision transition. + if (locals.elapsedSeconds >= locals.auction.core.auctionDurationSeconds) + { + switch (locals.auction.core.type) + { + case EAuctionType::Batch: + locals.finalizeBatchAuctionInput.auctionIndex = locals.auction.core.auctionIndex; + locals.finalizeBatchAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeBatchAuction, locals.finalizeBatchAuctionInput, locals.finalizeBatchAuctionOutput); + break; + case EAuctionType::Standard: + // No-bid and reserve-satisfying outcomes are deterministic and need no seller approval window. + if (locals.auction.core.highestBidAmount == 0 || locals.auction.core.highestBidPrice >= locals.auction.core.salePrice) + { + locals.finalizeStandardAuctionInput.auctionIndex = locals.auction.core.auctionIndex; + locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); + } + else + { + // A funded bid below the seller's sale price requires an explicit, time-bounded seller choice. + // Below-sale standard bids enter a seller decision window instead of settling immediately. + locals.auction.core.status = EAuctionStatus::PendingSellerDecision; + locals.auction.core.sellerDecisionDeadline = locals.currentDate; + locals.auction.core.sellerDecisionDeadline.add(0, 0, 0, 0, 0, NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS); + state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); + } + break; + default: break; + }; + } + break; + case EAuctionStatus::PendingSellerDecision: + switch (locals.auction.core.type) + { + case EAuctionType::Standard: + // Expiry resolves in favor of the recorded highest bidder so the seller cannot lock escrow indefinitely. + if (locals.auction.core.sellerDecisionDeadline <= locals.currentDate) + { + locals.finalizeStandardAuctionInput.auctionIndex = locals.auction.core.auctionIndex; + locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); + } + break; + default: break; + } + break; + default: break; + } + + locals.auctionIndex = state.get().auctionList.nextElementIndex(locals.auctionIndex); + } + } + + /** + * @brief Validates auction lot entries and totals the escrowed quantity. + */ + PRIVATE_FUNCTION_WITH_LOCALS(AnalyzeAuctionLot) + { + output.totalEscrowQuantity = 0; + output.lotItemCount = 0; + output.isValid = 0; + + // Lot validation also enforces the configured maximum auction lifetime. + if (input.durationDays == 0 || input.durationDays > state.get().maxAuctionDurationDays) + { + return; + } + + // Scan the full fixed ABI array because valid entries may be followed only by zero-padded slots. + for (locals.lotItemIndex = 0; locals.lotItemIndex < input.auctionLotItems.capacity(); ++locals.lotItemIndex) + { + locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); + // A zero asset is padding only when its paired quantity is also zero. + if (isZeroAsset(locals.lotItem.asset)) + { + if (locals.lotItem.quantity != 0) + { + return; + } + continue; + } + + if (locals.lotItem.quantity <= 0) + { + return; + } + + output.lotItemCount = sadd(output.lotItemCount, 1ULL); + output.totalEscrowQuantity = sadd(output.totalEscrowQuantity, static_cast(locals.lotItem.quantity)); + } + + output.isValid = output.lotItemCount > 0 ? 1 : 0; + } + + /** + * @brief Resolves whether the current tick belongs to a scheduled auction pause window. + */ + PRIVATE_FUNCTION_WITH_LOCALS(GetAuctionPauseState) + { + output.isPaused = 0; + output.pauseStartedAt.setInvalid(); + output.pauseEndsAt.setInvalid(); + + // The initial runtime date is treated as a full-day launch pause. + locals.currentDate = qpi.now(); + makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentDateStamp); + if (locals.currentDateStamp == NOST_DEFAULT_INIT_TIME) + { + output.isPaused = 1; + output.pauseStartedAt = locals.currentDate; + output.pauseStartedAt.setTime(0, 0, 0, 0, 0); + output.pauseEndsAt = output.pauseStartedAt; + output.pauseEndsAt.addDays(1); + return; + } + + // Scheduled pre-epoch pauses keep auctions from expiring during the transition window. + if (qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()) == NOST_PRE_EPOCH_PAUSE_DAY_OF_WEEK && qpi.hour() == NOST_PRE_EPOCH_PAUSE_HOUR && + qpi.minute() >= NOST_PRE_EPOCH_PAUSE_MINUTE) + { + output.isPaused = 1; + output.pauseStartedAt = locals.currentDate; + output.pauseStartedAt.setTime(NOST_PRE_EPOCH_PAUSE_HOUR, NOST_PRE_EPOCH_PAUSE_MINUTE, 0, 0, 0); + output.pauseEndsAt = output.pauseStartedAt; + output.pauseEndsAt.add(0, 0, 0, 0, 0, NOST_AUCTION_PRE_EPOCH_PAUSE_SECONDS); + } + } + + /** + * @brief Reports whether user-facing auction interactions are currently paused. + */ + PRIVATE_FUNCTION(IsAuctionInteractionPaused) + { + // Emergency pause takes precedence over scheduled and post-epoch launch pauses. + if (state.get().isEmergencyPaused) + { + output.isPaused = 1; + return; + } + + output.isPaused = state.get().isAuctionTimerPaused; + if (output.isPaused) + { + return; + } + + output.isPaused = state.get().isPostBeginEpochPauseArmed && (qpi.tick() - qpi.initialTick()) < NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS; + } + + /** + * @brief Synchronizes timer pause state and extends affected auction deadlines. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(SyncAuctionPauseState) + { + locals.currentDate = qpi.now(); + + // While emergency pause is active, keep extending the timer pause window. + if (state.get().isEmergencyPaused) + { + if (!state.get().isAuctionTimerPaused) + { + state.mut().isAuctionTimerPaused = 1; + state.mut().auctionTimerPauseStartedAt = locals.currentDate; + state.mut().auctionTimerPauseEndsAt = locals.currentDate; + } + else + { + state.mut().auctionTimerPauseEndsAt = locals.currentDate; + } + return; + } + + CALL(GetAuctionPauseState, locals.getAuctionPauseStateInput, locals.getAuctionPauseStateOutput); + + // The launch pause can overlap the scheduled pause; merge both windows before timers resume. + if (state.get().isPostBeginEpochPauseArmed) + { + if ((qpi.tick() - qpi.initialTick()) < NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) + { + if (!state.get().isAuctionTimerPaused) + { + state.mut().isAuctionTimerPaused = 1; + state.mut().auctionTimerPauseStartedAt = locals.currentDate; + state.mut().auctionTimerPauseEndsAt = locals.currentDate; + } + else + { + if (!state.get().auctionTimerPauseStartedAt.isValid()) + { + state.mut().auctionTimerPauseStartedAt = locals.currentDate; + } + if (!state.get().auctionTimerPauseEndsAt.isValid() || locals.currentDate > state.get().auctionTimerPauseEndsAt) + { + state.mut().auctionTimerPauseEndsAt = locals.currentDate; + } + } + + if (locals.getAuctionPauseStateOutput.isPaused) + { + if (!state.get().auctionTimerPauseStartedAt.isValid() || + locals.getAuctionPauseStateOutput.pauseStartedAt < state.get().auctionTimerPauseStartedAt) + { + state.mut().auctionTimerPauseStartedAt = locals.getAuctionPauseStateOutput.pauseStartedAt; + } + if (!state.get().auctionTimerPauseEndsAt.isValid() || + locals.getAuctionPauseStateOutput.pauseEndsAt > state.get().auctionTimerPauseEndsAt) + { + state.mut().auctionTimerPauseEndsAt = locals.getAuctionPauseStateOutput.pauseEndsAt; + } + } + return; + } + + state.mut().isPostBeginEpochPauseArmed = 0; + } + + // Scheduled pauses are recorded as a window that will later be added to all active deadlines. + if (locals.getAuctionPauseStateOutput.isPaused) + { + if (!state.get().isAuctionTimerPaused) + { + state.mut().isAuctionTimerPaused = 1; + state.mut().auctionTimerPauseStartedAt = locals.getAuctionPauseStateOutput.pauseStartedAt; + state.mut().auctionTimerPauseEndsAt = locals.getAuctionPauseStateOutput.pauseEndsAt; + return; + } + + if (!state.get().auctionTimerPauseStartedAt.isValid() || + locals.getAuctionPauseStateOutput.pauseStartedAt < state.get().auctionTimerPauseStartedAt) + { + state.mut().auctionTimerPauseStartedAt = locals.getAuctionPauseStateOutput.pauseStartedAt; + } + if (!state.get().auctionTimerPauseEndsAt.isValid() || locals.getAuctionPauseStateOutput.pauseEndsAt > state.get().auctionTimerPauseEndsAt) + { + state.mut().auctionTimerPauseEndsAt = locals.getAuctionPauseStateOutput.pauseEndsAt; + } + return; + } + + if (!state.get().isAuctionTimerPaused) + { + return; + } + + if (!state.get().auctionTimerPauseStartedAt.isValid() || !state.get().auctionTimerPauseEndsAt.isValid()) + { + state.mut().isAuctionTimerPaused = 0; + state.mut().auctionTimerPauseStartedAt.setInvalid(); + state.mut().auctionTimerPauseEndsAt.setInvalid(); + return; + } + + // When the pause ends, preserve elapsed auction time by extending every affected deadline. + diffDateInSecond(state.get().auctionTimerPauseStartedAt, state.get().auctionTimerPauseEndsAt, locals.pausedSeconds); + if (locals.pausedSeconds > 0) + { + locals.auctionIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); + while (locals.auctionIndex != NULL_INDEX) + { + locals.auction = state.get().auctionList.value(locals.auctionIndex); + if (locals.auction.core.status == EAuctionStatus::Active) + { + locals.auction.core.auctionDurationSeconds = sadd(locals.auction.core.auctionDurationSeconds, locals.pausedSeconds); + state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); + } + else if (locals.auction.core.status == EAuctionStatus::PendingSellerDecision && locals.auction.core.sellerDecisionDeadline.isValid()) + { + locals.auction.core.sellerDecisionDeadline.add(0, 0, 0, 0, 0, static_cast(locals.pausedSeconds)); + state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); + } + locals.auctionIndex = state.get().auctionList.nextElementIndex(locals.auctionIndex); + } + } + + state.mut().isAuctionTimerPaused = 0; + state.mut().auctionTimerPauseStartedAt.setInvalid(); + state.mut().auctionTimerPauseEndsAt.setInvalid(); + } + + /** + * @brief Returns remaining launch-delay ticks after the current epoch begins. + */ + PRIVATE_FUNCTION_WITH_LOCALS(GetTicksBeforeAuctionLaunchInternal) + { + output.ticks = 0; + + // An unarmed delay has no remaining ticks even if the current tick is near the epoch boundary. + if (!state.get().isPostBeginEpochPauseArmed) + { + return; + } + + output.ticks = static_cast(max(static_cast(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) - + (static_cast(qpi.tick()) - static_cast(qpi.initialTick())), + 0)); + } + + /** + * @brief Registers a QU obligation before the associated settlement becomes final. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(QueueQuPayout) + { + output.success = 0; + // Zero is an idempotent no-op, while a non-zero obligation must always have a payable recipient. + if (input.amount == 0 || isZero(input.recipient)) + { + output.success = input.amount == 0; + return; + } + + locals.previousAmount = 0; + // Reject aggregate overflow before touching either the per-wallet entry or its mirrored total. + if (input.amount > UINT64_MAX - state.get().totalPendingQuPayouts) + { + return; + } + + // Multiple settlements for one wallet share one liability entry to conserve bounded map capacity. + if (state.get().pendingQuPayouts.get(input.recipient, locals.previousAmount)) + { + // The wallet-level value must remain exactly reconcilable with totalPendingQuPayouts. + if (input.amount > UINT64_MAX - locals.previousAmount) + { + return; + } + + locals.updatedAmount = sadd(locals.previousAmount, input.amount); + if (!state.mut().pendingQuPayouts.replace(input.recipient, locals.updatedAmount)) + { + return; + } + } + else + { + // First-time recipients consume a new map slot; failure leaves the global liability total unchanged. + locals.payoutIndex = state.mut().pendingQuPayouts.set(input.recipient, input.amount); + if (locals.payoutIndex == NULL_INDEX) + { + return; + } + } + + state.mut().totalPendingQuPayouts = sadd(state.get().totalPendingQuPayouts, input.amount); + output.success = 1; + } + + /** + * @brief Pays a bounded number of chunks and preserves every unpaid remainder in state. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(FlushQuPayout) + { + output.success = 0; + output.transferredAmount = 0; + output.remainingAmount = 0; + // Absence is distinct from a paid zero balance because zero-balance entries are removed immediately. + if (!state.get().pendingQuPayouts.get(input.recipient, output.remainingAmount)) + { + return; + } + + locals.chunkIndex = 0; + // Bound both transfer size and iteration count so one payout attempt cannot exhaust contract execution time. + while (output.remainingAmount > 0 && locals.chunkIndex < input.maxChunks) + { + locals.chunkAmount = min(output.remainingAmount, static_cast(MAX_AMOUNT)); + locals.transferResult = qpi.transfer(input.recipient, static_cast(locals.chunkAmount)); + // A failed transfer stops delivery without decrementing the durable obligation. + if (locals.transferResult < 0) + { + break; + } + output.remainingAmount -= locals.chunkAmount; + output.transferredAmount = sadd(output.transferredAmount, locals.chunkAmount); + state.mut().totalPendingQuPayouts -= locals.chunkAmount; + ++locals.chunkIndex; + } + + // Fully discharged entries release map capacity; partial delivery persists the exact remainder for retry. + if (output.remainingAmount == 0) + { + state.mut().pendingQuPayouts.removeByKey(input.recipient); + } + else + { + state.mut().pendingQuPayouts.replace(input.recipient, output.remainingAmount); + } + output.success = 1; + } + + /** + * @brief Retries a bounded set of pending QU payouts and advances the persistent round-robin cursor. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(ProcessPendingQuPayouts) + { + locals.payoutScanIndex = mod(state.get().pendingPayoutScanCursor, state.get().pendingQuPayouts.capacity()); + locals.payoutTargetRecipientCount = min(state.get().pendingQuPayouts.population(), NOST_END_EPOCH_PAYOUT_RECIPIENT_NUM); + locals.processedPayoutRecipientCount = 0; + locals.payoutElementIndex = state.get().pendingQuPayouts.nextElementIndex(static_cast(locals.payoutScanIndex) - 1); + // Round-robin scanning bounds epoch work and prevents a permanently failing wallet from starving later map slots. + while (locals.processedPayoutRecipientCount < locals.payoutTargetRecipientCount) + { + // Wrap once the physical end is reached; the initial population snapshot prevents duplicate processing. + if (locals.payoutElementIndex == NULL_INDEX) + { + locals.payoutElementIndex = state.get().pendingQuPayouts.nextElementIndex(NULL_INDEX); + if (locals.payoutElementIndex == NULL_INDEX) + { + break; + } + } + locals.pendingPayoutRecipient = state.get().pendingQuPayouts.key(locals.payoutElementIndex); + locals.payoutScanIndex = mod(sadd(static_cast(locals.payoutElementIndex), 1ULL), state.get().pendingQuPayouts.capacity()); + locals.flushQuPayoutInput.recipient = locals.pendingPayoutRecipient; + locals.flushQuPayoutInput.maxChunks = NOST_END_EPOCH_PAYOUT_CHUNKS_PER_RECIPIENT; + CALL(FlushQuPayout, locals.flushQuPayoutInput, locals.flushQuPayoutOutput); + locals.processedPayoutRecipientCount = sadd(locals.processedPayoutRecipientCount, 1ULL); + locals.payoutElementIndex = state.get().pendingQuPayouts.nextElementIndex(locals.payoutElementIndex); + } + state.mut().pendingPayoutScanCursor = locals.payoutScanIndex; + } + + /** + * @brief Registers a payout exactly once for this call and immediately attempts bounded delivery. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(QueueAndFlushQuPayout) + { + output.success = 0; + output.transferredAmount = 0; + output.remainingAmount = 0; + locals.queueQuPayoutInput.recipient = input.recipient; + locals.queueQuPayoutInput.amount = input.amount; + CALL(QueueQuPayout, locals.queueQuPayoutInput, locals.queueQuPayoutOutput); + // Never attempt delivery unless the complete liability was made durable first. + if (!locals.queueQuPayoutOutput.success) + { + return; + } + // QueueQuPayout treats zero as success, but there is no map entry for FlushQuPayout to consume. + if (input.amount == 0) + { + output.success = 1; + return; + } + locals.flushQuPayoutInput.recipient = input.recipient; + locals.flushQuPayoutInput.maxChunks = input.maxChunks; + CALL(FlushQuPayout, locals.flushQuPayoutInput, locals.flushQuPayoutOutput); + output.transferredAmount = locals.flushQuPayoutOutput.transferredAmount; + output.remainingAmount = locals.flushQuPayoutOutput.remainingAmount; + output.success = locals.flushQuPayoutOutput.success; + } + + /** + * @brief Appends a participant snapshot to bounded history. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(ArchiveParticipant) + { + locals.historyIndex = mod(state.get().participantHistoryCounter, state.get().participantHistory.capacity()); + state.mut().participantHistory.set(locals.historyIndex, input.participantData); + state.mut().participantHistoryCounter = sadd(state.get().participantHistoryCounter, 1ULL); + } + + /** + * @brief Archives a closed auction and releases its active hash-map slot. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(ArchiveClosedAuction) + { + locals.historyIndex = mod(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); + state.mut().closedAuctionHistory.set(locals.historyIndex, input.auction); + state.mut().closedAuctionHistoryCounter = sadd(state.get().closedAuctionHistoryCounter, 1ULL); + state.mut().auctionList.removeByKey(input.auction.core.auctionIndex); + } + + /** + * @brief Finds a live auction or a retained closed-auction snapshot. + */ + PRIVATE_FUNCTION_WITH_LOCALS(FindAuction) + { + output.found = state.get().auctionList.get(input.auctionIndex, output.auction); + // Active storage is authoritative and avoids the bounded linear archive scan for live auctions. + if (output.found) + { + return; + } + // Closed auctions remain queryable only while their full snapshot is retained in the ring buffer. + for (locals.historyIndex = 0; locals.historyIndex < state.get().closedAuctionHistory.capacity(); ++locals.historyIndex) + { + locals.archivedAuction = state.get().closedAuctionHistory.get(locals.historyIndex); + if (locals.archivedAuction.core.status != EAuctionStatus::None && locals.archivedAuction.core.auctionIndex == input.auctionIndex) + { + output.auction = locals.archivedAuction; + output.found = 1; + return; + } + } + } + + /** + * @brief Tests retained closed history without copying an auction into the caller's locals. + */ + PRIVATE_FUNCTION_WITH_LOCALS(IsClosedAuctionRetained) + { + output.found = 0; + locals.retainedClosedAuctionCount = min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); + // Only initialized ring-buffer entries can match; inspect the const snapshot in place to keep this lookup lightweight. + for (locals.historyIndex = 0; locals.historyIndex < locals.retainedClosedAuctionCount; ++locals.historyIndex) + { + if (state.get().closedAuctionHistory.get(locals.historyIndex).core.status != EAuctionStatus::None && + state.get().closedAuctionHistory.get(locals.historyIndex).core.auctionIndex == input.auctionIndex) + { + output.found = 1; + return; + } + } + } + + /** + * @brief Selects the smallest retained auction index after an optional cursor. + */ + PRIVATE_FUNCTION_WITH_LOCALS(SelectNextRetainedAuction) + { + output.found = 0; + // Hash-map iteration is not creation ordered, so retain the smallest eligible live index beyond the cursor. + for (locals.auctionElementIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); locals.auctionElementIndex != NULL_INDEX; + locals.auctionElementIndex = state.get().auctionList.nextElementIndex(locals.auctionElementIndex)) + { + locals.candidateAuction = state.get().auctionList.value(locals.auctionElementIndex); + // Apply the cursor and optional seller filter before comparing creation indices. + if ((input.hasAfterAuctionIndex && locals.candidateAuction.core.auctionIndex <= input.afterAuctionIndex) || + (input.filterBySeller && locals.candidateAuction.core.seller != input.seller)) + { + continue; + } + if (!output.found || locals.candidateAuction.core.auctionIndex < output.auction.core.auctionIndex) + { + output.auction = locals.candidateAuction; + output.found = 1; + } + } + // Live-only callers avoid the archive scan entirely. + if (!input.includeClosedAuctions) + { + return; + } + + locals.retainedClosedAuctionCount = min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); + // Merge only retained closed snapshots without assuming physical ring order. + for (locals.historyIndex = 0; locals.historyIndex < locals.retainedClosedAuctionCount; ++locals.historyIndex) + { + locals.candidateAuction = state.get().closedAuctionHistory.get(locals.historyIndex); + // Apply the same cursor and seller filter to archived candidates. + if (locals.candidateAuction.core.status == EAuctionStatus::None || + (input.hasAfterAuctionIndex && locals.candidateAuction.core.auctionIndex <= input.afterAuctionIndex) || + (input.filterBySeller && locals.candidateAuction.core.seller != input.seller)) + { + continue; + } + if (!output.found || locals.candidateAuction.core.auctionIndex < output.auction.core.auctionIndex) + { + output.auction = locals.candidateAuction; + output.found = 1; + } + } + } + + /** + * @brief Counts retained live and closed auctions belonging to one seller without reconstructing creation order. + */ + PRIVATE_FUNCTION_WITH_LOCALS(CountRetainedAuctionsBySeller) + { + output.count = 0; + // A physical live-map pass is sufficient because counting does not depend on creation order. + for (locals.auctionElementIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); locals.auctionElementIndex != NULL_INDEX; + locals.auctionElementIndex = state.get().auctionList.nextElementIndex(locals.auctionElementIndex)) + { + locals.candidateAuction = state.get().auctionList.value(locals.auctionElementIndex); + if (locals.candidateAuction.core.seller == input.seller) + { + output.count = sadd(output.count, 1ULL); + } + } + + locals.retainedClosedAuctionCount = min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); + // Only initialized ring slots can contribute to the retained seller count. + for (locals.historyIndex = 0; locals.historyIndex < locals.retainedClosedAuctionCount; ++locals.historyIndex) + { + locals.candidateAuction = state.get().closedAuctionHistory.get(locals.historyIndex); + if (locals.candidateAuction.core.status != EAuctionStatus::None && locals.candidateAuction.core.seller == input.seller) + { + output.count = sadd(output.count, 1ULL); + } + } + } + + /** + * @brief Finds the smallest retained auction index whose complete fixed-size metadata CID matches the input. + */ + PRIVATE_FUNCTION_WITH_LOCALS(FindFirstRetainedAuctionByMetadataCid) + { + output.found = 0; + // Select the minimum matching live index directly instead of repeatedly reconstructing global order. + for (locals.auctionElementIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); locals.auctionElementIndex != NULL_INDEX; + locals.auctionElementIndex = state.get().auctionList.nextElementIndex(locals.auctionElementIndex)) + { + locals.candidateAuction = state.get().auctionList.value(locals.auctionElementIndex); + locals.metadataMatches = 1; + // Compare the complete fixed CID field, including zero padding. + for (locals.metadataIndex = 0; locals.metadataIndex < NOST_AUCTION_METADATA_CID_LENGTH; ++locals.metadataIndex) + { + if (locals.candidateAuction.core.metadataIpfsCid.get(locals.metadataIndex) != input.metadataIpfsCid.get(locals.metadataIndex)) + { + locals.metadataMatches = 0; + break; + } + } + if (locals.metadataMatches && (!output.found || locals.candidateAuction.core.auctionIndex < output.auction.core.auctionIndex)) + { + output.auction = locals.candidateAuction; + output.found = 1; + } + } + + locals.retainedClosedAuctionCount = min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); + // Closed snapshots share the same index ordering but occupy unordered ring slots. + for (locals.historyIndex = 0; locals.historyIndex < locals.retainedClosedAuctionCount; ++locals.historyIndex) + { + locals.candidateAuction = state.get().closedAuctionHistory.get(locals.historyIndex); + if (locals.candidateAuction.core.status == EAuctionStatus::None) + { + continue; + } + locals.metadataMatches = 1; + // Compare the complete fixed CID field, including zero padding. + for (locals.metadataIndex = 0; locals.metadataIndex < NOST_AUCTION_METADATA_CID_LENGTH; ++locals.metadataIndex) + { + if (locals.candidateAuction.core.metadataIpfsCid.get(locals.metadataIndex) != input.metadataIpfsCid.get(locals.metadataIndex)) + { + locals.metadataMatches = 0; + break; + } + } + if (locals.metadataMatches && (!output.found || locals.candidateAuction.core.auctionIndex < output.auction.core.auctionIndex)) + { + output.auction = locals.candidateAuction; + output.found = 1; + } + } + } + + /** + * @brief Pays auction sale proceeds to the seller and records every fee for end-of-epoch settlement. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(DistributeAuctionRevenue) + { + output.sellerPayout = input.grossAmount; + output.success = 0; + + // Zero-gross settlements still report success so callers can close no-sale auctions cleanly. + if (input.grossAmount == 0) + { + output.success = 1; + return; + } + // Only the seller is queued during settlement; fee recipients are handled by END_EPOCH. + if (state.get().pendingQuPayouts.population() > state.get().pendingQuPayouts.capacity() - NOST_AUCTION_REVENUE_MAX_PAYOUT_RECIPIENTS) + { + return; + } + + calculateAuctionRevenueBreakdown(input.grossAmount, state, locals.auctionRevenueBreakdown); + output.sellerPayout = locals.auctionRevenueBreakdown.sellerPayout; + + // Register the seller liability before recording fees so a queue-capacity failure cannot duplicate fee accrual on retry. + locals.payoutInput.recipient = input.seller; + locals.payoutInput.amount = output.sellerPayout; + locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } + + locals.feePool = state.get().feePool; + // The routing decision and fee configuration are captured when revenue is settled; recipient wallets are resolved at END_EPOCH. + if (routeAllFeesToDevelopment(state)) + { + locals.feePool.developmentAmount = sadd(locals.feePool.developmentAmount, input.grossAmount - output.sellerPayout); + } + else + { + locals.shareholderFeeTierIndex = getAuctionShareholderFeeTierIndex(input.grossAmount); + switch (locals.shareholderFeeTierIndex) + { + case 0: + locals.feePool.shareholderDividendTier1Amount = + sadd(locals.feePool.shareholderDividendTier1Amount, locals.auctionRevenueBreakdown.shareholderDividendAmount); + break; + case 1: + locals.feePool.shareholderDividendTier2Amount = + sadd(locals.feePool.shareholderDividendTier2Amount, locals.auctionRevenueBreakdown.shareholderDividendAmount); + break; + case 2: + locals.feePool.shareholderDividendTier3Amount = + sadd(locals.feePool.shareholderDividendTier3Amount, locals.auctionRevenueBreakdown.shareholderDividendAmount); + break; + default: + locals.feePool.shareholderDividendTier4Amount = + sadd(locals.feePool.shareholderDividendTier4Amount, locals.auctionRevenueBreakdown.shareholderDividendAmount); + break; + } + + locals.feePool.managementAmount = sadd(locals.feePool.managementAmount, locals.auctionRevenueBreakdown.managementFeeAmount); + locals.feePool.developmentAmount = sadd(locals.feePool.developmentAmount, locals.auctionRevenueBreakdown.developmentFeeAmount); + locals.feePool.takeoverCoordinatorAmount = + sadd(locals.feePool.takeoverCoordinatorAmount, locals.auctionRevenueBreakdown.takeoverCoordinatorFeeAmount); + } + + state.mut().feePool = locals.feePool; + output.success = 1; + } + + /** + * @brief Accumulates a service fee using the routing mode active when the fee is charged. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(AccumulateAuctionServiceFee) + { + output.success = 0; + + // Creation, bidding, and cancellation paths may call this with zero after fee configuration changes. + if (input.feeAmount == 0) + { + output.success = 1; + return; + } + + locals.feePool = state.get().feePool; + if (routeAllFeesToDevelopment(state)) + { + locals.feePool.developmentAmount = sadd(locals.feePool.developmentAmount, input.feeAmount); + } + else + { + locals.feePool.commonServiceFeeAmount = sadd(locals.feePool.commonServiceFeeAmount, input.feeAmount); + } + state.mut().feePool = locals.feePool; + output.success = 1; + } + + /** + * @brief Materializes compatible service fees and settles every shared pool accumulator using the recipients active at `END_EPOCH`. + * @note Each accumulator is cleared only after its value has moved to dividend dust or a durable payout liability. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(DistributeNostromoFeePool) + { + output.success = 0; + locals.feePool = state.get().feePool; + + if (locals.feePool.commonServiceFeeAmount > 0) + { + calculateAuctionServiceFeeBreakdown(locals.feePool.commonServiceFeeAmount, locals.auctionServiceFeeBreakdown); + locals.feePool.shareholderDividendAmount = + sadd(locals.feePool.shareholderDividendAmount, locals.auctionServiceFeeBreakdown.shareholderDividendAmount); + locals.feePool.managementAmount = sadd(locals.feePool.managementAmount, locals.auctionServiceFeeBreakdown.managementFeeAmount); + locals.feePool.developmentAmount = sadd(locals.feePool.developmentAmount, locals.auctionServiceFeeBreakdown.developmentFeeAmount); + locals.feePool.takeoverCoordinatorAmount = + sadd(locals.feePool.takeoverCoordinatorAmount, locals.auctionServiceFeeBreakdown.takeoverCoordinatorFeeAmount); + locals.feePool.commonServiceFeeAmount = 0; + state.mut().feePool = locals.feePool; + } + + locals.shareholderDividendAmount = + sadd(sadd(sadd(locals.feePool.shareholderDividendTier1Amount, locals.feePool.shareholderDividendTier2Amount), + sadd(locals.feePool.shareholderDividendTier3Amount, locals.feePool.shareholderDividendTier4Amount)), + locals.feePool.shareholderDividendAmount); + if (locals.shareholderDividendAmount > 0) + { + state.mut().auctionShareholderDividendPool = sadd(state.get().auctionShareholderDividendPool, locals.shareholderDividendAmount); + locals.feePool.shareholderDividendTier1Amount = 0; + locals.feePool.shareholderDividendTier2Amount = 0; + locals.feePool.shareholderDividendTier3Amount = 0; + locals.feePool.shareholderDividendTier4Amount = 0; + locals.feePool.shareholderDividendAmount = 0; + state.mut().feePool = locals.feePool; + } + + locals.dividendPerShare = div(state.get().auctionShareholderDividendPool, NUMBER_OF_COMPUTORS); + if (locals.dividendPerShare > 0 && qpi.distributeDividends(locals.dividendPerShare)) + { + locals.distributedDividendAmount = smul(locals.dividendPerShare, static_cast(NUMBER_OF_COMPUTORS)); + state.mut().auctionShareholderDividendPool -= locals.distributedDividendAmount; + } + + if (state.get().feePool.managementAmount > 0) + { + locals.payoutInput.recipient = state.get().management; + locals.payoutInput.amount = state.get().feePool.managementAmount; + locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } + state.mut().feePool.managementAmount = 0; + } + if (state.get().feePool.developmentAmount > 0) + { + locals.payoutInput.recipient = state.get().development; + locals.payoutInput.amount = state.get().feePool.developmentAmount; + locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } + state.mut().feePool.developmentAmount = 0; + } + if (state.get().feePool.takeoverCoordinatorAmount > 0) + { + locals.payoutInput.recipient = state.get().takeoverCoordinator; + locals.payoutInput.amount = state.get().feePool.takeoverCoordinatorAmount; + locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } + state.mut().feePool.takeoverCoordinatorAmount = 0; + } + + output.success = 1; + } + + /** + * @brief Counts non-empty wallet entries allowed to bid in a private auction. + */ + PRIVATE_FUNCTION_WITH_LOCALS(CountAllowedBidderWallets) + { + output.allowedWalletCount = 0; + for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < input.allowedBidderWallets.capacity(); ++locals.allowedWalletIndex) + { + if (!isZero(input.allowedBidderWallets.get(locals.allowedWalletIndex))) + { + output.allowedWalletCount = sadd(output.allowedWalletCount, 1ULL); + } + } + } + + /** + * @brief Counts valid access-asset requirements for private auction gating. + */ + PRIVATE_FUNCTION_WITH_LOCALS(CountRequiredAccessAssets) + { + output.requiredAccessAssetCount = 0; + output.isValid = 1; + // Empty asset slots are allowed only when their quantity is also empty. + for (locals.requiredAccessAssetIndex = 0; locals.requiredAccessAssetIndex < input.requiredAccessAssets.capacity(); + ++locals.requiredAccessAssetIndex) + { + locals.requiredAccessAsset = input.requiredAccessAssets.get(locals.requiredAccessAssetIndex); + if (isZeroAsset(locals.requiredAccessAsset.asset)) + { + if (locals.requiredAccessAsset.quantity != 0) + { + output.isValid = 0; + return; + } + continue; + } + + if (locals.requiredAccessAsset.quantity <= 0) + { + output.isValid = 0; + return; + } + + output.requiredAccessAssetCount = sadd(output.requiredAccessAssetCount, 1ULL); + } + } + + /** + * @brief Checks whether the invocator owns at least one configured access asset. + */ + PRIVATE_FUNCTION_WITH_LOCALS(HasRequiredAccessAsset) + { + output.hasRequiredAccessAsset = 0; + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + return; + } + + // Owning any one configured access asset at the required quantity grants private auction access. + for (locals.requiredAccessAssetSetIndex = locals.auction.requiredAccessAssets.nextElementIndex(NULL_INDEX); + locals.requiredAccessAssetSetIndex != NULL_INDEX; + locals.requiredAccessAssetSetIndex = locals.auction.requiredAccessAssets.nextElementIndex(locals.requiredAccessAssetSetIndex)) + { + locals.requiredAccessAsset.asset = locals.auction.requiredAccessAssets.key(locals.requiredAccessAssetSetIndex); + locals.requiredAccessAsset.quantity = locals.auction.requiredAccessAssets.value(locals.requiredAccessAssetSetIndex); + locals.possessedAccessShares = qpi.numberOfShares(locals.requiredAccessAsset.asset, AssetOwnershipSelect::byOwner(qpi.invocator()), + AssetPossessionSelect::byPossessor(qpi.invocator())); + if (locals.possessedAccessShares >= locals.requiredAccessAsset.quantity) + { + output.hasRequiredAccessAsset = 1; + return; + } + } + } + + /** + * @brief Recomputes the displayed highest active Batch Auction bid. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(RecomputeBatchHighestBid) + { + locals.bestParticipantFound = 0; + + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + return; + } + + if (locals.auction.core.type != EAuctionType::Batch) + { + return; + } + + // Batch auctions expose the highest active price, with FIFO tie-breaking for equal bids. + for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) + { + locals.participantData = state.get().participants.get(locals.participantIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) + { + continue; + } + + if (!locals.participantData.isActive || locals.participantData.escrowedAmount == 0) + { + continue; + } + + if (!locals.bestParticipantFound || locals.participantData.bidAmount > locals.bestParticipantData.bidAmount || + (locals.participantData.bidAmount == locals.bestParticipantData.bidAmount && + locals.participantData.bidIndex < locals.bestParticipantData.bidIndex)) + { + locals.bestParticipantFound = 1; + locals.bestParticipantData = locals.participantData; + locals.bestParticipantSlotIndex = locals.participantIndex; + } + } + + if (locals.bestParticipantFound) + { + locals.auction.core.highestBidder = locals.bestParticipantData.participant; + locals.auction.core.highestBidPrice = locals.bestParticipantData.bidAmount; + locals.auction.core.highestBidQuantity = locals.bestParticipantData.requestedQuantity; + locals.auction.core.highestBidAmount = locals.bestParticipantData.escrowedAmount; + locals.auction.core.highestBidSlotIndex = locals.bestParticipantSlotIndex; + } + else + { + locals.auction.core.highestBidAmount = 0; + locals.auction.core.highestBidPrice = 0; + locals.auction.core.highestBidQuantity = 0; + locals.auction.core.highestBidder = NULL_ID; + locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; + } + + state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); + } + + /** + * @brief Computes the price and quantity still available for a Batch Auction bid. + */ + PRIVATE_FUNCTION_WITH_LOCALS(ComputeBatchBidAvailability) + { + output.found = 0; + output.isAcceptingBids = 0; + output.minimumBidPrice = 0; + output.availableQuantity = 0; + locals.lowestWinningPriceFound = 0; + locals.lowestWinningPrice = 0; + locals.salePriorityQuantity = 0; + locals.priorityQuantity = 0; + + // Availability is defined only for a retained live auction; closed snapshots never accept bids. + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + return; + } + + output.found = 1; + if (locals.auction.core.type != EAuctionType::Batch || locals.auction.core.status != EAuctionStatus::Active || + locals.auction.core.quantityForSale < locals.auction.core.minimumPurchaseQuantity) + { + return; + } + + // Existing sale-price-or-better bids reserve priority quantity before a new bid can enter. + for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) + { + locals.participantData = state.get().participants.get(locals.participantIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) + { + continue; + } + + if (!locals.participantData.isActive || locals.participantData.escrowedAmount == 0 || locals.participantData.requestedQuantity == 0) + { + continue; + } + + if (!locals.lowestWinningPriceFound || locals.participantData.bidAmount < locals.lowestWinningPrice) + { + locals.lowestWinningPriceFound = 1; + locals.lowestWinningPrice = locals.participantData.bidAmount; + } + + if (locals.participantData.bidAmount >= locals.auction.core.salePrice) + { + locals.salePriorityQuantity = sadd(locals.salePriorityQuantity, locals.participantData.requestedQuantity); + } + } + + locals.effectiveCoverageQuantity = + locals.auction.core.quantityForSale - locals.auction.core.minimumPurchaseQuantity + NOST_BATCH_COVERAGE_THRESHOLD_OFFSET; + // Once less than one minimum allocation remains, report no sale-price capacity instead of an unusable fragment. + if (locals.salePriorityQuantity >= locals.effectiveCoverageQuantity) + { + output.availableQuantity = 0; + } + else + { + // Otherwise expose the full unreserved quantity; the minimum check below decides whether bidding remains viable. + output.availableQuantity = locals.auction.core.quantityForSale - locals.salePriorityQuantity; + } + + // If sale-price capacity is exhausted, new bids must improve the current lowest winning price. + if (output.availableQuantity >= locals.auction.core.minimumPurchaseQuantity) + { + output.minimumBidPrice = locals.auction.core.salePrice; + output.isAcceptingBids = 1; + } + else + { + // A full book can still accept a strictly better bid that displaces the current lowest-priced allocation. + output.availableQuantity = 0; + if (!locals.lowestWinningPriceFound || locals.lowestWinningPrice == UINT64_MAX) + { + return; + } + + output.minimumBidPrice = sadd(locals.lowestWinningPrice, 1ULL); + output.isAcceptingBids = 1; + if (input.bidAmount == 0) + { + return; + } + } + + locals.outputPrice = input.bidAmount > 0 ? input.bidAmount : output.minimumBidPrice; + if (locals.outputPrice < output.minimumBidPrice) + { + output.availableQuantity = 0; + return; + } + + // Recompute capacity at the requested price so callers know the maximum acceptable quantity. + locals.priorityQuantity = 0; + for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) + { + locals.participantData = state.get().participants.get(locals.participantIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) + { + continue; + } + + if (!locals.participantData.isActive || locals.participantData.escrowedAmount == 0 || locals.participantData.requestedQuantity == 0) + { + continue; + } + + if (locals.participantData.bidAmount > locals.outputPrice || locals.participantData.bidAmount == locals.outputPrice) + { + locals.priorityQuantity = sadd(locals.priorityQuantity, locals.participantData.requestedQuantity); + } + } + + // Equal-priced existing bids have FIFO priority, so a candidate at that price receives only later capacity. + if (locals.priorityQuantity >= locals.auction.core.quantityForSale) + { + output.availableQuantity = 0; + return; + } + + output.availableQuantity = locals.auction.core.quantityForSale - locals.priorityQuantity; + } + + /** + * @brief Validates, escrows, and ranks a new Batch Auction bid. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(ProcessBatchBid) + { + output.escrowedAmount = 0; + output.refundedAmount = 0; + output.errorCode = EAuctionError::Success; + output.success = 0; + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::AuctionNotFound; + return; + } + + if (input.effectiveQuantity < locals.auction.core.minimumPurchaseQuantity || input.bidAmount == 0) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::InvalidInput; + return; + } + // Retain enough payout slots to refund every active participant plus the caller's possible overpayment. + if (state.get().pendingQuPayouts.population() > + state.get().pendingQuPayouts.capacity() - state.get().participants.capacity() - NOST_BATCH_BID_CALLER_PAYOUT_RECIPIENTS) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::PayoutQueueFull; + return; + } + + calculateBatchAuctionBidFee(input.effectiveQuantity, input.bidAmount, locals.bidFeeCalculation); + if (locals.bidFeeCalculation.escrowAmount == 0) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::InvalidInput; + return; + } + + if (input.bidAmount < locals.auction.core.salePrice) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::BidTooLow; + return; + } + + locals.computeBatchBidAvailabilityInput.auctionIndex = input.auctionIndex; + locals.computeBatchBidAvailabilityInput.bidAmount = input.bidAmount; + CALL(ComputeBatchBidAvailability, locals.computeBatchBidAvailabilityInput, locals.computeBatchBidAvailabilityOutput); + if (!locals.computeBatchBidAvailabilityOutput.isAcceptingBids || input.bidAmount < locals.computeBatchBidAvailabilityOutput.minimumBidPrice) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::BidTooLow; + return; + } + if (input.effectiveQuantity > locals.computeBatchBidAvailabilityOutput.availableQuantity) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::QuantityUnavailable; + return; + } + + if (static_cast(qpi.invocationReward()) < locals.bidFeeCalculation.requiredReward) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::InsufficientFunds; + return; + } + + // Batch bids consume live slots only; displaced and settled records move to the history ring. + locals.freeParticipantSlotFound = 0; + for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) + { + locals.participantData = state.get().participants.get(locals.participantIndex); + if (!locals.participantData.isUsed) + { + locals.freeParticipantSlotFound = 1; + locals.freeParticipantSlotIndex = locals.participantIndex; + break; + } + } + + if (!locals.freeParticipantSlotFound || locals.auction.core.nextBidIndex == UINT64_MAX) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::StorageFull; + return; + } + + locals.participantData.escrowedAmount = locals.bidFeeCalculation.escrowAmount; + locals.participantData.requestedQuantity = input.effectiveQuantity; + locals.participantData.allocatedQuantity = 0; + locals.participantData.bidAmount = input.bidAmount; + locals.participantData.lastBidTime = input.currentDate; + locals.participantData.participant = qpi.invocator(); + locals.participantData.auctionIndex = input.auctionIndex; + locals.participantData.bidIndex = locals.auction.core.nextBidIndex; + locals.participantData.isUsed = 1; + locals.participantData.isActive = 1; + locals.participantData.isWinningBid = 1; + + // Accepted bids near deadline extend the auction to reduce last-moment sniping. + locals.auction.core.lastBidAt = input.currentDate; + if ((locals.auction.core.auctionDurationSeconds - input.elapsedSeconds) <= NOST_AUCTION_EXTENSION_SECONDS) + { + locals.auction.core.auctionDurationSeconds = sadd(locals.auction.core.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); + } + + locals.auction.core.nextBidIndex = sadd(locals.auction.core.nextBidIndex, 1ULL); + state.mut().participants.set(locals.freeParticipantSlotIndex, locals.participantData); + state.mut().auctionList.replace(input.auctionIndex, locals.auction); + + // Keep only the highest-priority quantity active; displaced escrow is refunded immediately. + locals.activeQuantity = 0; + for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) + { + locals.participantData = state.get().participants.get(locals.participantIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) + { + continue; + } + if (locals.participantData.isActive && locals.participantData.escrowedAmount > 0 && locals.participantData.requestedQuantity > 0) + { + locals.activeQuantity = sadd(locals.activeQuantity, locals.participantData.requestedQuantity); + } + } + + // Repeatedly evict the lowest-priority tail until active demand fits the finite lot supply. + while (locals.activeQuantity > locals.auction.core.quantityForSale) + { + locals.worstParticipantFound = 0; + // Lowest price loses first; for equal prices the newest bid loses to preserve FIFO priority. + for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) + { + locals.participantData = state.get().participants.get(locals.participantIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) + { + continue; + } + + if (!locals.participantData.isActive || locals.participantData.escrowedAmount == 0 || locals.participantData.requestedQuantity == 0) + { + continue; + } + + if (!locals.worstParticipantFound || locals.participantData.bidAmount < locals.worstParticipantData.bidAmount || + (locals.participantData.bidAmount == locals.worstParticipantData.bidAmount && + locals.participantData.bidIndex > locals.worstParticipantData.bidIndex)) + { + locals.worstParticipantFound = 1; + locals.worstParticipantData = locals.participantData; + locals.worstParticipantSlotIndex = locals.participantIndex; + } + } + + if (!locals.worstParticipantFound) + { + break; + } + + locals.excessQuantity = locals.activeQuantity - locals.auction.core.quantityForSale; + locals.displacedQuantity = min(locals.excessQuantity, locals.worstParticipantData.requestedQuantity); + locals.displacedRefund = smul(locals.displacedQuantity, locals.worstParticipantData.bidAmount); + locals.remainingWorstQuantity = locals.worstParticipantData.requestedQuantity - locals.displacedQuantity; + // A partial order smaller than the minimum is removed in full; keeping it would create an invalid final allocation. + if (locals.remainingWorstQuantity > 0 && locals.remainingWorstQuantity < locals.auction.core.minimumPurchaseQuantity) + { + locals.displacedQuantity = locals.worstParticipantData.requestedQuantity; + locals.displacedRefund = locals.worstParticipantData.escrowedAmount; + } + // Full displacement retires the live slot; partial displacement keeps a valid minimum-sized order active. + if (locals.displacedQuantity >= locals.worstParticipantData.requestedQuantity) + { + locals.worstParticipantData.escrowedAmount = 0; + locals.worstParticipantData.requestedQuantity = 0; + locals.worstParticipantData.allocatedQuantity = 0; + locals.worstParticipantData.isActive = 0; + locals.worstParticipantData.isWinningBid = 0; + } + else + { + locals.worstParticipantData.requestedQuantity -= locals.displacedQuantity; + locals.worstParticipantData.escrowedAmount -= locals.displacedRefund; + locals.worstParticipantData.isWinningBid = 1; + } + + if (locals.displacedRefund > 0) + { + locals.payoutInput.recipient = locals.worstParticipantData.participant; + locals.payoutInput.amount = locals.displacedRefund; + locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + output.refundedAmount = sadd(output.refundedAmount, locals.displacedRefund); + } + locals.activeQuantity -= locals.displacedQuantity; + // Archive only retired orders; partially displaced orders remain in the live priority book. + if (!locals.worstParticipantData.isActive) + { + locals.archiveParticipantInput.participantData = locals.worstParticipantData; + CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); + locals.worstParticipantData = {}; + } + state.mut().participants.set(locals.worstParticipantSlotIndex, locals.worstParticipantData); + } + + locals.recomputeBatchHighestBidInput.auctionIndex = input.auctionIndex; + CALL(RecomputeBatchHighestBid, locals.recomputeBatchHighestBidInput, locals.recomputeBatchHighestBidOutput); + + // Small-bid service fees are retained even if the bid is later displaced. + if (locals.bidFeeCalculation.fee > 0) + { + locals.accumulateAuctionServiceFeeInput.feeAmount = locals.bidFeeCalculation.fee; + CALL(AccumulateAuctionServiceFee, locals.accumulateAuctionServiceFeeInput, locals.accumulateAuctionServiceFeeOutput); + } + + if (static_cast(qpi.invocationReward()) > locals.bidFeeCalculation.requiredReward) + { + locals.payoutInput.recipient = qpi.invocator(); + locals.payoutInput.amount = static_cast(qpi.invocationReward()) - locals.bidFeeCalculation.requiredReward; + locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + output.refundedAmount = + sadd(output.refundedAmount, static_cast(qpi.invocationReward()) - locals.bidFeeCalculation.requiredReward); + } + + output.escrowedAmount = locals.bidFeeCalculation.escrowAmount; + output.success = 1; + } + + /** + * @brief Validates and records a Standard Auction bid, refunding replaced escrow. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(ProcessStandardBid) + { + output.escrowedAmount = 0; + output.refundedAmount = 0; + output.errorCode = EAuctionError::Success; + output.success = 0; + locals.highestBidderExists = 0; + locals.finalizeImmediately = 0; + locals.participantExists = 0; + locals.freeParticipantSlotFound = 0; + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + output.errorCode = EAuctionError::AuctionNotFound; + return; + } + + if (locals.auction.core.quantityForSale == 0 || locals.auction.core.quantityForSale < locals.auction.core.minimumPurchaseQuantity || + input.bidAmount == 0) + { + output.errorCode = EAuctionError::InvalidInput; + return; + } + // Reserve distinct entries for a replaced bidder, bidder change, three fee wallets, and the seller. + // This also guarantees that an accepted Buy Now bid can complete settlement in the same call. + if (state.get().pendingQuPayouts.population() > state.get().pendingQuPayouts.capacity() - NOST_STANDARD_BID_MAX_PAYOUT_RECIPIENTS) + { + output.errorCode = EAuctionError::PayoutQueueFull; + return; + } + + locals.requiredEscrow = input.bidAmount; + if (static_cast(qpi.invocationReward()) < locals.requiredEscrow) + { + output.errorCode = EAuctionError::InsufficientFunds; + return; + } + + if (locals.auction.core.highestBidPrice == 0) + { + if (input.bidAmount < locals.auction.core.initialPrice) + { + output.errorCode = EAuctionError::BidTooLow; + return; + } + } + else if (input.bidAmount < sadd(locals.auction.core.highestBidPrice, locals.auction.core.minimumBidIncrement)) + { + output.errorCode = EAuctionError::BidTooLow; + return; + } + + // Standard bidders update their own active slot, while a new bidder needs one reusable slot. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) + { + locals.participantData = state.get().participants.get(locals.participantSlotIndex); + if (locals.participantData.isUsed && locals.participantData.isActive && locals.participantData.auctionIndex == input.auctionIndex && + locals.participantData.participant == qpi.invocator()) + { + locals.participantExists = 1; + break; + } + if (!locals.freeParticipantSlotFound && !locals.participantData.isUsed) + { + locals.freeParticipantSlotFound = 1; + locals.freeParticipantSlotIndex = locals.participantSlotIndex; + } + } + locals.previousEscrow = locals.participantExists ? locals.participantData.escrowedAmount : 0; + if (!locals.participantExists && !locals.freeParticipantSlotFound) + { + output.errorCode = EAuctionError::StorageFull; + return; + } + if (!locals.participantExists) + { + locals.participantSlotIndex = locals.freeParticipantSlotIndex; + if (locals.auction.core.nextBidIndex == UINT64_MAX) + { + output.errorCode = EAuctionError::StorageFull; + return; + } + } + + locals.participantData.escrowedAmount = locals.requiredEscrow; + locals.participantData.requestedQuantity = locals.auction.core.quantityForSale; + locals.participantData.allocatedQuantity = 0; + locals.participantData.bidAmount = input.bidAmount; + locals.participantData.lastBidTime = input.currentDate; + locals.participantData.participant = qpi.invocator(); + locals.participantData.auctionIndex = input.auctionIndex; + locals.participantData.bidIndex = locals.participantExists ? locals.participantData.bidIndex : locals.auction.core.nextBidIndex; + locals.participantData.isUsed = 1; + locals.participantData.isActive = 1; + locals.participantData.isWinningBid = 0; + if (!locals.participantExists) + { + locals.auction.core.nextBidIndex = sadd(locals.auction.core.nextBidIndex, 1ULL); + } + + // A new highest bid releases the previous bidder's escrow before storing the replacement. + locals.highestBidderSlotIndex = locals.auction.core.highestBidSlotIndex; + if (locals.highestBidderSlotIndex < state.get().participants.capacity()) + { + locals.previousHighestBidderData = state.get().participants.get(locals.highestBidderSlotIndex); + locals.highestBidderExists = locals.previousHighestBidderData.isUsed && locals.previousHighestBidderData.isActive && + locals.previousHighestBidderData.auctionIndex == input.auctionIndex; + } + if (locals.highestBidderExists && locals.previousHighestBidderData.participant != qpi.invocator()) + { + locals.payoutInput.recipient = locals.previousHighestBidderData.participant; + locals.payoutInput.amount = locals.previousHighestBidderData.escrowedAmount; + locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + output.refundedAmount = sadd(output.refundedAmount, locals.previousHighestBidderData.escrowedAmount); + locals.previousHighestBidderData.escrowedAmount = 0; + locals.previousHighestBidderData.requestedQuantity = 0; + locals.previousHighestBidderData.isActive = 0; + locals.previousHighestBidderData.isWinningBid = 0; + locals.archiveParticipantInput.participantData = locals.previousHighestBidderData; + CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); + locals.previousHighestBidderData = {}; + state.mut().participants.set(locals.highestBidderSlotIndex, locals.previousHighestBidderData); + } + + locals.participantData.isWinningBid = 1; + locals.auction.core.highestBidder = qpi.invocator(); + locals.auction.core.highestBidPrice = input.bidAmount; + locals.auction.core.highestBidQuantity = locals.auction.core.quantityForSale; + locals.auction.core.highestBidAmount = locals.requiredEscrow; + locals.auction.core.highestBidSlotIndex = locals.participantSlotIndex; + + locals.auction.core.lastBidAt = input.currentDate; + if ((locals.auction.core.auctionDurationSeconds - input.elapsedSeconds) <= NOST_AUCTION_EXTENSION_SECONDS) + { + locals.auction.core.auctionDurationSeconds = sadd(locals.auction.core.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); + } + if (locals.auction.core.buyNowPrice > 0 && input.bidAmount >= locals.auction.core.buyNowPrice) + { + locals.finalizeImmediately = 1; + } + + state.mut().participants.set(locals.participantSlotIndex, locals.participantData); + state.mut().auctionList.replace(input.auctionIndex, locals.auction); + + // Refund replaced self-escrow and excess reward after the new bid state is durable. + if (locals.previousEscrow > 0) + { + locals.payoutInput.recipient = qpi.invocator(); + locals.payoutInput.amount = locals.previousEscrow; + locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + output.refundedAmount = sadd(output.refundedAmount, locals.previousEscrow); + } + if (static_cast(qpi.invocationReward()) > locals.requiredEscrow) + { + locals.payoutInput.recipient = qpi.invocator(); + locals.payoutInput.amount = static_cast(qpi.invocationReward()) - locals.requiredEscrow; + locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + output.refundedAmount = sadd(output.refundedAmount, static_cast(qpi.invocationReward()) - locals.requiredEscrow); + } + + output.escrowedAmount = locals.requiredEscrow; + output.success = 1; + + // Buy Now closes the auction in the same procedure after the winning bid is recorded. + if (locals.finalizeImmediately) + { + locals.finalizeStandardAuctionInput.auctionIndex = input.auctionIndex; + locals.finalizeStandardAuctionInput.currentDate = input.currentDate; + CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); + } + } + + /** + * @brief Validates the fixed-size IPFS metadata CID field. + */ + PRIVATE_FUNCTION_WITH_LOCALS(ValidateMetadataCid) + { + output.isValid = 0; + locals.hasPayloadCharacters = 0; + locals.reachedTerminator = 0; + + // Nostromo stores lowercase base32 CIDv1 values, which begin with the multibase prefix `b`. + if (input.metadataIpfsCid.get(0) != QPI::Ch::b) + { + return; + } + + // After the first zero byte, the fixed-size CID field must remain zero-padded. + for (locals.cidIndex = 1; locals.cidIndex < input.metadataIpfsCid.capacity(); ++locals.cidIndex) + { + locals.cidChar = input.metadataIpfsCid.get(locals.cidIndex); + if (locals.cidChar == 0) + { + locals.reachedTerminator = 1; + continue; + } + + if (locals.reachedTerminator) + { + return; + } + + if ((locals.cidChar >= QPI::Ch::a && locals.cidChar <= QPI::Ch::z) || (locals.cidChar >= QPI::Ch::_2 && locals.cidChar <= QPI::Ch::_7)) + { + locals.hasPayloadCharacters = 1; + continue; + } + + return; + } + + if (!locals.hasPayloadCharacters) + { + return; + } + + output.isValid = 1; + } + + /** + * @brief Verifies that the invocator can escrow every non-empty lot asset. + */ + PRIVATE_FUNCTION_WITH_LOCALS(VerifyAuctionLotBalances) + { + output.hasEnoughBalance = 1; + // Creation validates possession before attempting escrow so failures can refund without rollback. + for (locals.lotItemIndex = 0; locals.lotItemIndex < input.auctionLotItems.capacity(); ++locals.lotItemIndex) + { + locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); + if (isZeroAsset(locals.lotItem.asset) || locals.lotItem.quantity <= 0) + { + continue; + } + + locals.possessedShares = qpi.numberOfPossessedShares(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, qpi.invocator(), + qpi.invocator(), SELF_INDEX, SELF_INDEX); + if (locals.possessedShares < locals.lotItem.quantity) + { + output.hasEnoughBalance = 0; + return; + } + } + } + + /** + * @brief Returns escrowed lot assets to the specified recipient. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(RollbackAuctionLotAssets) + { + // Rollback is shared by cancellation, failed creation, rejected standard sales, and no-sale finalization. + for (locals.lotItemIndex = 0; locals.lotItemIndex < input.auctionLotItems.capacity(); ++locals.lotItemIndex) + { + locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); + if (isZeroAsset(locals.lotItem.asset) || locals.lotItem.quantity <= 0) + { + continue; + } + qpi.transferShareOwnershipAndPossession(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, SELF, SELF, locals.lotItem.quantity, + input.recipient); + } + } + + /** + * @brief Settles a Batch Auction by allocating winning quantities and closing the auction. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(FinalizeBatchAuction) + { + output.success = 0; + locals.bestParticipantFound = 0; + locals.lotItemFound = 0; + locals.soldQuantity = 0; + locals.totalGrossAmount = 0; + + // Abort if the auction no longer exists or is no longer an active batch auction. + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + return; + } + + if (locals.auction.core.type != EAuctionType::Batch || locals.auction.core.status != EAuctionStatus::Active) + { + return; + } + if (state.get().pendingQuPayouts.population() > + state.get().pendingQuPayouts.capacity() - state.get().participants.capacity() - NOST_AUCTION_REVENUE_MAX_PAYOUT_RECIPIENTS) + { + return; + } + + // Resolve the single sellable lot entry that represents the batch asset and quantity in escrow. + for (locals.lotItemIndex = 0; locals.lotItemIndex < locals.auction.core.auctionLotItems.capacity(); ++locals.lotItemIndex) + { + locals.batchLotItem = locals.auction.core.auctionLotItems.get(locals.lotItemIndex); + if (!isZeroAsset(locals.batchLotItem.asset) && locals.batchLotItem.quantity > 0) + { + locals.lotItemFound = 1; + break; + } + } + if (!locals.lotItemFound) + { + return; + } + + // Stop before producing a fragment below the auction minimum; the remainder stays with the seller. + locals.remainingQuantity = locals.auction.core.quantityForSale; + while (locals.remainingQuantity >= locals.auction.core.minimumPurchaseQuantity) + { + locals.bestParticipantFound = 0; + locals.participantIndex = 0; + + // Price priority is descending; the monotonic bid index is the only FIFO tie-breaker. + while (locals.participantIndex < state.get().participants.capacity()) + { + locals.participantData = state.get().participants.get(locals.participantIndex); + if (locals.participantData.isUsed && locals.participantData.auctionIndex == input.auctionIndex) + { + if (locals.participantData.isActive && locals.participantData.escrowedAmount > 0) + { + if (!locals.bestParticipantFound || locals.participantData.bidAmount > locals.bestParticipantData.bidAmount || + (locals.participantData.bidAmount == locals.bestParticipantData.bidAmount && + locals.participantData.bidIndex < locals.bestParticipantData.bidIndex)) + { + locals.bestParticipantFound = 1; + locals.bestParticipantData = locals.participantData; + locals.bestParticipantSlotIndex = locals.participantIndex; + } + } + } + ++locals.participantIndex; + } + + if (!locals.bestParticipantFound) + { + break; + } + + // Price the winning allocation and compute any escrow surplus that must be returned immediately. + locals.allocatedQuantity = min(locals.remainingQuantity, locals.bestParticipantData.requestedQuantity); + locals.requiredPayment = smul(locals.allocatedQuantity, locals.bestParticipantData.bidAmount); + locals.refundAmount = 0; + if (locals.bestParticipantData.escrowedAmount > locals.requiredPayment) + { + locals.refundAmount = locals.bestParticipantData.escrowedAmount - locals.requiredPayment; + } + + // Transfer the awarded shares, mark the participant as a winner, and advance settlement totals. + if (locals.allocatedQuantity > 0) + { + qpi.transferShareOwnershipAndPossession(locals.batchLotItem.asset.assetName, locals.batchLotItem.asset.issuer, SELF, SELF, + locals.allocatedQuantity, locals.bestParticipantData.participant); + locals.bestParticipantData.allocatedQuantity = locals.allocatedQuantity; + locals.bestParticipantData.isWinningBid = 1; + locals.soldQuantity = sadd(locals.soldQuantity, locals.allocatedQuantity); + locals.totalGrossAmount = sadd(locals.totalGrossAmount, locals.requiredPayment); + locals.remainingQuantity -= locals.allocatedQuantity; + } + + // Return the unused part of the winner escrow when the participant requested more than the remaining supply. + if (locals.refundAmount > 0) + { + locals.payoutInput.recipient = locals.bestParticipantData.participant; + locals.payoutInput.amount = locals.refundAmount; + locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } + } + + // Archive the completed bid and release its active slot immediately. + locals.bestParticipantData.escrowedAmount = 0; + locals.bestParticipantData.isActive = 0; + locals.archiveParticipantInput.participantData = locals.bestParticipantData; + CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); + locals.bestParticipantData = {}; + state.mut().participants.set(locals.bestParticipantSlotIndex, locals.bestParticipantData); + } + + // Refund every non-winning or non-allocated bid that still has escrow locked after winner selection. + locals.participantIndex = 0; + while (locals.participantIndex < state.get().participants.capacity()) + { + locals.participantData = state.get().participants.get(locals.participantIndex); + if (locals.participantData.isUsed && locals.participantData.auctionIndex == input.auctionIndex) + { + if (locals.participantData.escrowedAmount > 0) + { + locals.payoutInput.recipient = locals.participantData.participant; + locals.payoutInput.amount = locals.participantData.escrowedAmount; + locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } + locals.participantData.escrowedAmount = 0; + locals.participantData.allocatedQuantity = 0; + locals.participantData.isWinningBid = 0; + } + locals.participantData.isActive = 0; + locals.archiveParticipantInput.participantData = locals.participantData; + CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); + locals.participantData = {}; + state.mut().participants.set(locals.participantIndex, locals.participantData); + } + ++locals.participantIndex; + } + + // Return any unsold batch quantity to the seller when demand did not consume the entire lot. + if (locals.soldQuantity < locals.auction.core.quantityForSale) + { + qpi.transferShareOwnershipAndPossession(locals.batchLotItem.asset.assetName, locals.batchLotItem.asset.issuer, SELF, SELF, + locals.auction.core.quantityForSale - locals.soldQuantity, locals.auction.core.seller); + } + + // Split the collected proceeds according to Nostromo auction fee rules and pay the seller net amount. + locals.distributeAuctionRevenueInput.seller = locals.auction.core.seller; + locals.distributeAuctionRevenueInput.grossAmount = locals.totalGrossAmount; + CALL(DistributeAuctionRevenue, locals.distributeAuctionRevenueInput, locals.distributeAuctionRevenueOutput); + if (!locals.distributeAuctionRevenueOutput.success) + { + return; + } + + // Persist the final sold quantity and close the auction as settled. + locals.auction.core.allocatedQuantity = locals.soldQuantity; + locals.auction.core.status = EAuctionStatus::Finalized; + locals.auction.core.settledAt = input.currentDate; + locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; + state.mut().totalFinalizedAuctions = sadd(state.get().totalFinalizedAuctions, 1ULL); + locals.archiveClosedAuctionInput.auction = locals.auction; + CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); + output.success = 1; + } + + /** + * @brief Settles a Standard Auction by transferring the lot or returning it to the seller. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(FinalizeStandardAuction) + { + output.success = 0; + locals.highestBidderExists = 0; + locals.lotSold = 0; + + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + return; + } + + if (locals.auction.core.type != EAuctionType::Standard) + { + return; + } + if (state.get().pendingQuPayouts.population() > state.get().pendingQuPayouts.capacity() - NOST_STANDARD_FINALIZATION_MAX_PAYOUT_RECIPIENTS) + { + return; + } + + locals.highestBidderSlotIndex = locals.auction.core.highestBidSlotIndex; + if (locals.highestBidderSlotIndex < state.get().participants.capacity()) + { + locals.highestBidderData = state.get().participants.get(locals.highestBidderSlotIndex); + locals.highestBidderExists = + locals.highestBidderData.isUsed && locals.highestBidderData.isActive && locals.highestBidderData.auctionIndex == input.auctionIndex; + } + + // A valid highest bid transfers the whole standard lot and treats escrow as gross proceeds. + if (locals.highestBidderExists && locals.highestBidderData.escrowedAmount > 0) + { + locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = locals.highestBidderData.participant; + CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); + + locals.distributeAuctionRevenueInput.seller = locals.auction.core.seller; + locals.distributeAuctionRevenueInput.grossAmount = locals.highestBidderData.escrowedAmount; + CALL(DistributeAuctionRevenue, locals.distributeAuctionRevenueInput, locals.distributeAuctionRevenueOutput); + if (!locals.distributeAuctionRevenueOutput.success) + { + return; + } + + locals.highestBidderData.allocatedQuantity = locals.auction.core.quantityForSale; + locals.highestBidderData.isWinningBid = 1; + locals.highestBidderData.escrowedAmount = 0; + locals.highestBidderData.isActive = 0; + locals.archiveParticipantInput.participantData = locals.highestBidderData; + CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); + locals.highestBidderData = {}; + state.mut().participants.set(locals.highestBidderSlotIndex, locals.highestBidderData); + locals.auction.core.allocatedQuantity = locals.auction.core.quantityForSale; + locals.lotSold = 1; + } + else + { + // No active funded bid means the seller receives the lot back with no revenue distribution. + locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.core.seller; + CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); + locals.auction.core.allocatedQuantity = 0; + } + + // Closed standard auctions retain winner fields only when the lot actually sold. + locals.auction.core.status = EAuctionStatus::Finalized; + locals.auction.core.settledAt = input.currentDate; + if (!locals.lotSold) + { + locals.auction.core.highestBidAmount = 0; + locals.auction.core.highestBidPrice = 0; + locals.auction.core.highestBidQuantity = 0; + locals.auction.core.highestBidder = NULL_ID; + } + locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; + state.mut().totalFinalizedAuctions = sadd(state.get().totalFinalizedAuctions, 1ULL); + locals.archiveClosedAuctionInput.auction = locals.auction; + CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); + output.success = 1; + } + + /** + * @brief Rejects a pending Standard Auction bid and closes the auction without a sale. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(RejectStandardAuction) + { + output.refundedAmount = 0; + output.success = 0; + locals.highestBidderExists = 0; + + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + return; + } + + if (locals.auction.core.type != EAuctionType::Standard || locals.auction.core.status != EAuctionStatus::PendingSellerDecision) + { + return; + } + if (state.get().pendingQuPayouts.population() == state.get().pendingQuPayouts.capacity()) + { + return; + } + + locals.highestBidderSlotIndex = locals.auction.core.highestBidSlotIndex; + if (locals.highestBidderSlotIndex < state.get().participants.capacity()) + { + locals.highestBidderData = state.get().participants.get(locals.highestBidderSlotIndex); + locals.highestBidderExists = + locals.highestBidderData.isUsed && locals.highestBidderData.isActive && locals.highestBidderData.auctionIndex == input.auctionIndex; + } + + // Seller rejection unwinds the pending bid instead of distributing its escrow as proceeds. + if (locals.highestBidderExists && locals.highestBidderData.escrowedAmount > 0) + { + locals.payoutInput.recipient = locals.highestBidderData.participant; + locals.payoutInput.amount = locals.highestBidderData.escrowedAmount; + locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } + output.refundedAmount = locals.highestBidderData.escrowedAmount; + locals.highestBidderData.escrowedAmount = 0; + locals.highestBidderData.allocatedQuantity = 0; + locals.highestBidderData.isActive = 0; + locals.highestBidderData.isWinningBid = 0; + locals.archiveParticipantInput.participantData = locals.highestBidderData; + CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); + locals.highestBidderData = {}; + state.mut().participants.set(locals.highestBidderSlotIndex, locals.highestBidderData); + } + + // The seller keeps the lot after rejection, and the auction is closed as finalized. + locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.core.seller; + CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); + + locals.auction.core.allocatedQuantity = 0; + locals.auction.core.highestBidAmount = 0; + locals.auction.core.highestBidPrice = 0; + locals.auction.core.highestBidQuantity = 0; + locals.auction.core.highestBidder = NULL_ID; + locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; + locals.auction.core.status = EAuctionStatus::Finalized; + locals.auction.core.settledAt = input.currentDate; + state.mut().totalFinalizedAuctions = sadd(state.get().totalFinalizedAuctions, 1ULL); + locals.archiveClosedAuctionInput.auction = locals.auction; + CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); + output.success = 1; + } + + /** + * @brief Transfers auction lot assets into contract escrow during creation. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(EscrowAuctionLotAssets) + { + output.success = 1; + // Escrow entries one by one; a later failure rolls back earlier successful transfers. + for (locals.lotItemIndex = 0; locals.lotItemIndex < input.auctionLotItems.capacity(); ++locals.lotItemIndex) + { + locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); + if (isZeroAsset(locals.lotItem.asset) || locals.lotItem.quantity <= 0) + { + continue; + } + + locals.remainingShares = qpi.transferShareOwnershipAndPossession(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, + qpi.invocator(), qpi.invocator(), locals.lotItem.quantity, SELF); + if (locals.remainingShares < 0) + { + // `transferShareOwnershipAndPossession` returns the remaining number of matching shares after a successful transfer. + // Negative values mean the transfer failed without moving the requested lot entry. + for (locals.rollbackLotItemIndex = 0; locals.rollbackLotItemIndex < locals.lotItemIndex; ++locals.rollbackLotItemIndex) + { + locals.lotItem = input.auctionLotItems.get(locals.rollbackLotItemIndex); + if (isZeroAsset(locals.lotItem.asset) || locals.lotItem.quantity <= 0) + { + continue; + } + qpi.transferShareOwnershipAndPossession(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, SELF, SELF, + locals.lotItem.quantity, qpi.invocator()); + } + output.success = 0; + return; + } + } + } + + /** + * @brief Creates a new Batch Auction or Standard Auction in the Nostromo Auction House. + * @note `CreateAuction_input` defines the IPFS metadata CID stored through Pinata, the auction lot, pricing, duration, and visibility rules. + * @note Batch auctions require `minimumPurchaseQuantity` in the range `[1, quantityForSale]`; standard auctions ignore it and store zero. + * @note A successful public Batch or Standard Auction accumulates the configured public creation fee, distributed at `END_EPOCH`. + * Insufficient payment rejects creation, overpayment is refunded, and failed creation refunds the full reward. + * @note Private auctions require the configured private auction fee, which is accumulated and distributed at `END_EPOCH` between shareholders + * and the configured fee recipients, and must use at least one access mode. If both modes are configured, either one grants access. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(CreateAuction) + { + output.errorCode = EAuctionError::InvalidInput; + + // Any rejection before escrow succeeds refunds the full invocation reward. + CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); + if (locals.isAuctionInteractionPausedOutput.isPaused) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::AuctionPaused; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + if (!isSupportedAuctionType(static_cast(input.auctionType))) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidAuctionType; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + if (!isSupportedAuctionVisibility(static_cast(input.auctionVisibility))) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidVisibility; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + if (state.get().auctionList.population() >= state.get().auctionList.capacity()) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::StorageFull; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + if (state.get().totalAuctionsCreated == UINT64_MAX) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::AuctionIndexExhausted; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + locals.validateMetadataCidInput.metadataIpfsCid = input.metadataIpfsCid; + CALL(ValidateMetadataCid, locals.validateMetadataCidInput, locals.validateMetadataCidOutput); + if (!locals.validateMetadataCidOutput.isValid) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + locals.analyzeAuctionLotInput.auctionLotItems = input.auctionLotItems; + locals.analyzeAuctionLotInput.durationDays = input.durationDays; + CALL(AnalyzeAuctionLot, locals.analyzeAuctionLotInput, locals.analyzeAuctionLotOutput); + if (!locals.analyzeAuctionLotOutput.isValid) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + // Resolve auction-type-specific quantity and price invariants before touching assets. + locals.resolvedQuantityForSale = 0; + locals.resolvedMinimumPurchaseQuantity = 0; + switch (static_cast(input.auctionType)) + { + case EAuctionType::Batch: + + if (!resolveBatchAuctionCreateParams(locals.analyzeAuctionLotOutput.lotItemCount, locals.analyzeAuctionLotOutput.totalEscrowQuantity, + input.minimumPurchaseQuantity, locals.resolvedQuantityForSale, + locals.resolvedMinimumPurchaseQuantity, input.buyNowPrice)) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + break; + case EAuctionType::Standard: + if (!resolveStandardAuctionCreateParams(input.minimumBidIncrement, locals.resolvedQuantityForSale, + locals.resolvedMinimumPurchaseQuantity, input.buyNowPrice, input.initialPrice, + input.salePrice)) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + break; + default: + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidAuctionType; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + // Private auctions require at least one access gate and may combine wallet and asset access. + locals.countAllowedBidderWalletsInput.allowedBidderWallets = input.allowedBidderWallets; + CALL(CountAllowedBidderWallets, locals.countAllowedBidderWalletsInput, locals.countAllowedBidderWalletsOutput); + locals.countRequiredAccessAssetsInput.requiredAccessAssets = input.requiredAccessAssets; + CALL(CountRequiredAccessAssets, locals.countRequiredAccessAssetsInput, locals.countRequiredAccessAssetsOutput); + if (!locals.countRequiredAccessAssetsOutput.isValid || + !validatePrivateAuctionAccess(static_cast(input.auctionVisibility), + locals.countRequiredAccessAssetsOutput.requiredAccessAssetCount, + locals.countAllowedBidderWalletsOutput.allowedWalletCount)) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + locals.requiredFee = getCreateAuctionFee(static_cast(input.auctionVisibility), state); + if (qpi.invocationReward() < locals.requiredFee) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - if (qpi.invocationReward() > NOSTROMO_CREATE_PROJECT_FEE) + output.errorCode = EAuctionError::InsufficientFunds; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + locals.verifyAuctionLotBalancesInput.auctionLotItems = input.auctionLotItems; + CALL(VerifyAuctionLotBalances, locals.verifyAuctionLotBalancesInput, locals.verifyAuctionLotBalancesOutput); + if (!locals.verifyAuctionLotBalancesOutput.hasEnoughBalance) + { + if (qpi.invocationReward() > 0) { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - NOSTROMO_CREATE_PROJECT_FEE); + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - state.mut().epochRevenue += NOSTROMO_CREATE_PROJECT_FEE; + output.errorCode = EAuctionError::InsufficientAssetBalance; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } - locals.newProject.creator = qpi.invocator(); - locals.newProject.tokenName = input.tokenName; - locals.newProject.supplyOfToken = input.supply; - locals.newProject.startDate = locals.startDate; - locals.newProject.endDate = locals.endDate; - locals.newProject.numberOfYes = 0; - locals.newProject.numberOfNo = 0; + // From this point onward, asset escrow may need explicit rollback on storage failure. + locals.escrowAuctionLotAssetsInput.auctionLotItems = input.auctionLotItems; + CALL(EscrowAuctionLotAssets, locals.escrowAuctionLotAssetsInput, locals.escrowAuctionLotAssetsOutput); + if (!locals.escrowAuctionLotAssetsOutput.success) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InsufficientAssetBalance; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } - output.indexOfProject = state.get().numberOfCreatedProject; - state.mut().projects.set(state.get().numberOfCreatedProject, locals.newProject); - state.mut().numberOfCreatedProject++; - state.mut().tokens.add(input.tokenName); + locals.auction.core.auctionIndex = state.get().totalAuctionsCreated; + locals.auction.core.quantityForSale = locals.resolvedQuantityForSale; + locals.auction.core.minimumPurchaseQuantity = locals.resolvedMinimumPurchaseQuantity; + locals.auction.core.initialPrice = input.initialPrice; + locals.auction.core.salePrice = input.salePrice; + locals.auction.core.minimumBidIncrement = input.minimumBidIncrement; + locals.auction.core.buyNowPrice = input.buyNowPrice; + locals.auction.core.auctionDurationSeconds = smul(static_cast(input.durationDays), NOST_SECONDS_PER_DAY); + locals.auction.core.createdAt = qpi.now(); + locals.auction.core.lastBidAt = locals.auction.core.createdAt; + locals.auction.core.seller = qpi.invocator(); + locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; + // Duplicate required access assets collapse to the highest configured quantity. + for (locals.requiredAccessAssetIndex = 0; locals.requiredAccessAssetIndex < input.requiredAccessAssets.capacity(); + ++locals.requiredAccessAssetIndex) + { + locals.requiredAccessAsset = input.requiredAccessAssets.get(locals.requiredAccessAssetIndex); + if (!isZeroAsset(locals.requiredAccessAsset.asset) && + (!locals.auction.requiredAccessAssets.get(locals.requiredAccessAsset.asset, locals.existingRequiredAccessQuantity) || + locals.requiredAccessAsset.quantity > locals.existingRequiredAccessQuantity)) + { + locals.auction.requiredAccessAssets.set(locals.requiredAccessAsset.asset, locals.requiredAccessAsset.quantity); + } } - else + locals.auction.core.auctionLotItems = input.auctionLotItems; + for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < input.allowedBidderWallets.capacity(); ++locals.allowedWalletIndex) + { + if (!isZero(input.allowedBidderWallets.get(locals.allowedWalletIndex))) + { + locals.auction.allowedBidderWallets.add(input.allowedBidderWallets.get(locals.allowedWalletIndex)); + } + } + locals.auction.core.metadataIpfsCid = input.metadataIpfsCid; + locals.auction.core.type = static_cast(input.auctionType); + locals.auction.core.visibility = static_cast(input.auctionVisibility); + locals.auction.core.status = EAuctionStatus::Active; + + // If persistent auction storage fails after escrow, return the lot before refunding the fee reward. + if (state.mut().auctionList.set(locals.auction.core.auctionIndex, locals.auction) == NULL_INDEX) { + locals.rollbackAuctionLotAssetsInput.auctionLotItems = input.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = qpi.invocator(); + CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.indexOfProject = NOSTROMO_MAX_NUMBER_PROJECT; + output.errorCode = EAuctionError::StorageFull; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + // Creation fees are held until END_EPOCH; overpayment is returned immediately. + if (locals.requiredFee > 0) + { + locals.accumulateAuctionServiceFeeInput.feeAmount = static_cast(locals.requiredFee); + CALL(AccumulateAuctionServiceFee, locals.accumulateAuctionServiceFeeInput, locals.accumulateAuctionServiceFeeOutput); + } + + if (qpi.invocationReward() > locals.requiredFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.requiredFee); } + + output.auctionIndex = locals.auction.core.auctionIndex; + state.mut().totalAuctionsCreated = sadd(state.get().totalAuctionsCreated, 1ULL); + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, qpi.invocationReward()); + logProcedureResult(locals.log); } - struct voteInProject_locals + /** + * @brief Places a bid in an active auction. + * @note Batch auctions interpret `bidAmount` as price per asset and reject requested `quantity` below `minimumPurchaseQuantity` with a full + * refund. + * @note An accepted Batch bid escrows `quantity * bidAmount` and accumulates `max(100 - quantity * bidAmount, 0)` qu for distribution at + * `END_EPOCH`. Excess reward is refunded; rejected bids refund the full reward. The accumulated fee is not refunded if the bid is later + * displaced. + * @note Batch final allocations are also at least `minimumPurchaseQuantity`; smaller unsold remainders return to the seller and affected bids are + * fully refunded. + * @note Standard auctions interpret `bidAmount` as the total price for the whole lot and ignore `quantity`. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(PlaceBid) { - projectInfo votedProject; - Array votedList; - uint32 elementIndex, curDate, numberOfVotedProject, i; - bit flag; - }; + output.errorCode = EAuctionError::InvalidInput; - PUBLIC_PROCEDURE_WITH_LOCALS(voteInProject) - { - if (input.indexOfProject >= state.get().numberOfCreatedProject) + // Common auction gates run before type-specific bid processing; failed gates refund the reward. + CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); + if (locals.isAuctionInteractionPausedOutput.isPaused) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::AuctionPaused; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); + logProcedureResult(locals.log); + return; + } + + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { - return ; + locals.findAuctionInput.auctionIndex = input.auctionIndex; + CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = locals.findAuctionOutput.found ? EAuctionError::AuctionClosed : EAuctionError::AuctionNotFound; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); + logProcedureResult(locals.log); + return; } - if (state.get().users.contains(qpi.invocator()) == 0) + + if (locals.auction.core.status != EAuctionStatus::Active) { - return ; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::AuctionClosed; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); + logProcedureResult(locals.log); + return; } - state.get().numberOfVotedProject.get(qpi.invocator(), locals.numberOfVotedProject); - if (locals.numberOfVotedProject == NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST) + + if (locals.auction.core.seller == qpi.invocator()) { - return ; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); + logProcedureResult(locals.log); + return; } - state.get().voteStatus.get(qpi.invocator(), locals.votedList); - for (locals.i = 0; locals.i < locals.numberOfVotedProject; locals.i++) + + locals.currentDate = qpi.now(); + diffDateInSecond(locals.auction.core.createdAt, locals.currentDate, locals.elapsedSeconds); + if (locals.elapsedSeconds >= locals.auction.core.auctionDurationSeconds) { - if (locals.votedList.get(locals.i) == input.indexOfProject) + if (qpi.invocationReward() > 0) { - return ; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + output.errorCode = EAuctionError::AuctionClosed; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); + logProcedureResult(locals.log); + return; } - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); - if (locals.curDate >= state.get().projects.get(input.indexOfProject).startDate && locals.curDate < state.get().projects.get(input.indexOfProject).endDate) + + // When both gates are configured, satisfying either one grants access. + if (locals.auction.core.visibility == EAuctionVisibility::Private) { - locals.votedProject = state.get().projects.get(input.indexOfProject); - if (input.decision) + locals.hasAccess = locals.auction.allowedBidderWallets.population() > 0 && locals.auction.allowedBidderWallets.contains(qpi.invocator()); + if (!locals.hasAccess && locals.auction.requiredAccessAssets.population() > 0) { - locals.votedProject.numberOfYes++; + locals.hasRequiredAccessAssetInput.auctionIndex = input.auctionIndex; + CALL(HasRequiredAccessAsset, locals.hasRequiredAccessAssetInput, locals.hasRequiredAccessAssetOutput); + locals.hasAccess = locals.hasRequiredAccessAssetOutput.hasRequiredAccessAsset; } - else + + if (!locals.hasAccess) { - locals.votedProject.numberOfNo++; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::PrivateAuctionAccessDenied; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, + output.escrowedAmount); + logProcedureResult(locals.log); + return; } - state.mut().projects.set(input.indexOfProject, locals.votedProject); - locals.votedList.set(locals.numberOfVotedProject++, input.indexOfProject); - state.mut().voteStatus.set(qpi.invocator(), locals.votedList); - state.mut().numberOfVotedProject.set(qpi.invocator(), locals.numberOfVotedProject); } - } - struct createFundraising_locals - { - projectInfo tmpProject; - fundaraisingInfo newFundraising; - uint32 curDate, firstPhaseStartDate, firstPhaseEndDate, secondPhaseStartDate, secondPhaseEndDate, thirdPhaseStartDate, thirdPhaseEndDate, listingStartDate, cliffEndDate, vestingEndDate; - }; + // Type-specific processors own escrow/refund details once common validation succeeds. + switch (locals.auction.core.type) + { + case EAuctionType::Batch: + locals.processBatchBidInput.auctionIndex = input.auctionIndex; + locals.processBatchBidInput.effectiveQuantity = input.quantity; + locals.processBatchBidInput.bidAmount = input.bidAmount; + locals.processBatchBidInput.currentDate = locals.currentDate; + locals.processBatchBidInput.elapsedSeconds = locals.elapsedSeconds; + CALL(ProcessBatchBid, locals.processBatchBidInput, locals.processBatchBidOutput); + if (!locals.processBatchBidOutput.success) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.refundedAmount = locals.processBatchBidOutput.refundedAmount; + output.errorCode = locals.processBatchBidOutput.errorCode; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, + output.escrowedAmount); + logProcedureResult(locals.log); + return; + } + output.refundedAmount = sadd(output.refundedAmount, locals.processBatchBidOutput.refundedAmount); + output.escrowedAmount = locals.processBatchBidOutput.escrowedAmount; + break; + case EAuctionType::Standard: + locals.processStandardBidInput.auctionIndex = input.auctionIndex; + locals.processStandardBidInput.bidAmount = input.bidAmount; + locals.processStandardBidInput.currentDate = locals.currentDate; + locals.processStandardBidInput.elapsedSeconds = locals.elapsedSeconds; + CALL(ProcessStandardBid, locals.processStandardBidInput, locals.processStandardBidOutput); + if (!locals.processStandardBidOutput.success) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = locals.processStandardBidOutput.errorCode; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, + output.escrowedAmount); + logProcedureResult(locals.log); + return; + } + output.refundedAmount = sadd(output.refundedAmount, locals.processStandardBidOutput.refundedAmount); + output.escrowedAmount = locals.processStandardBidOutput.escrowedAmount; + break; + default: + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidAuctionType; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, + output.escrowedAmount); + logProcedureResult(locals.log); + return; + } + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); + logProcedureResult(locals.log); + } - PUBLIC_PROCEDURE_WITH_LOCALS(createFundraising) + /** + * @brief Cancels an active auction before the first accepted bid is placed. + * @note Once any bid is accepted, the seller can no longer cancel the auction. + * @note The cancellation fee is based on the configured reserve price for the full batch quantity or standard lot and is distributed between + * shareholders and the configured fee recipients. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(CancelAuction) { - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); - packNostromoDate(input.firstPhaseStartYear, input.firstPhaseStartMonth, input.firstPhaseStartDay, input.firstPhaseStartHour, 0, 0, locals.firstPhaseStartDate); - packNostromoDate(input.secondPhaseStartYear, input.secondPhaseStartMonth, input.secondPhaseStartDay, input.secondPhaseStartHour, 0, 0, locals.secondPhaseStartDate); - packNostromoDate(input.thirdPhaseStartYear, input.thirdPhaseStartMonth, input.thirdPhaseStartDay, input.thirdPhaseStartHour, 0, 0, locals.thirdPhaseStartDate); - packNostromoDate(input.firstPhaseEndYear, input.firstPhaseEndMonth, input.firstPhaseEndDay, input.firstPhaseEndHour, 0, 0, locals.firstPhaseEndDate); - packNostromoDate(input.secondPhaseEndYear, input.secondPhaseEndMonth, input.secondPhaseEndDay, input.secondPhaseEndHour, 0, 0, locals.secondPhaseEndDate); - packNostromoDate(input.thirdPhaseEndYear, input.thirdPhaseEndMonth, input.thirdPhaseEndDay, input.thirdPhaseEndHour, 0, 0, locals.thirdPhaseEndDate); - packNostromoDate(input.listingStartYear, input.listingStartMonth, input.listingStartDay, input.listingStartHour, 0, 0, locals.listingStartDate); - packNostromoDate(input.cliffEndYear, input.cliffEndMonth, input.cliffEndDay, input.cliffEndHour, 0, 0, locals.cliffEndDate); - packNostromoDate(input.vestingEndYear, input.vestingEndMonth, input.vestingEndDay, input.vestingEndHour, 0, 0, locals.vestingEndDate); + output.refundedAmount = 0; + output.cancellationFee = 0; + output.errorCode = EAuctionError::InvalidInput; - if (locals.curDate > locals.firstPhaseStartDate || locals.firstPhaseStartDate >= locals.firstPhaseEndDate || locals.firstPhaseEndDate > locals.secondPhaseStartDate || locals.secondPhaseStartDate >= locals.secondPhaseEndDate || locals.secondPhaseEndDate > locals.thirdPhaseStartDate || locals.thirdPhaseStartDate >= locals.thirdPhaseEndDate || locals.thirdPhaseEndDate > locals.listingStartDate || locals.listingStartDate > locals.cliffEndDate || locals.cliffEndDate > locals.vestingEndDate) + // Cancellation is blocked during emergency pause but does not use the scheduled auction timer pause. + if (state.get().isEmergencyPaused) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = EAuctionError::AuctionPaused; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); + logProcedureResult(locals.log); + return; } - if (checkValidNostDateTime(locals.firstPhaseStartDate) == 0 || checkValidNostDateTime(locals.firstPhaseEndDate) == 0 || checkValidNostDateTime(locals.secondPhaseStartDate) == 0 || checkValidNostDateTime(locals.secondPhaseEndDate) == 0 || checkValidNostDateTime(locals.thirdPhaseStartDate) == 0 || checkValidNostDateTime(locals.thirdPhaseEndDate) == 0 || checkValidNostDateTime(locals.listingStartDate) == 0 || checkValidNostDateTime(locals.cliffEndDate) == 0 || checkValidNostDateTime(locals.vestingEndDate) == 0) + + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { + locals.findAuctionInput.auctionIndex = input.auctionIndex; + CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = locals.findAuctionOutput.found ? EAuctionError::AuctionClosed : EAuctionError::AuctionNotFound; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); + logProcedureResult(locals.log); + return; } - if (input.stepOfVesting == 0 || input.stepOfVesting > 12 || input.TGE > 50 || input.threshold > 50 || input.indexOfProject >= state.get().numberOfCreatedProject) + if (locals.auction.core.status != EAuctionStatus::Active) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = EAuctionError::AuctionClosed; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); + logProcedureResult(locals.log); + return; } - - if (state.get().projects.get(input.indexOfProject).creator != qpi.invocator()) + if (locals.auction.core.seller != qpi.invocator()) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); + logProcedureResult(locals.log); + return; } - if (input.soldAmount > state.get().projects.get(input.indexOfProject).supplyOfToken) + if (locals.auction.core.nextBidIndex != 0) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = EAuctionError::AuctionHasAcceptedBid; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); + logProcedureResult(locals.log); + return; } - if (locals.curDate <= state.get().projects.get(input.indexOfProject).endDate || state.get().projects.get(input.indexOfProject).numberOfYes <= state.get().projects.get(input.indexOfProject).numberOfNo || state.get().projects.get(input.indexOfProject).isCreatedFundarasing == 1) + // The fee base represents the full reserve value of the lot being withdrawn. + locals.cancellationBaseAmount = locals.auction.core.salePrice; + if (locals.auction.core.type == EAuctionType::Batch) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; + locals.cancellationBaseAmount = smul(locals.auction.core.salePrice, locals.auction.core.quantityForSale); } + output.cancellationFee = calculateBasisPointAmount(locals.cancellationBaseAmount, state.get().auctionCancellationFeeBasisPoints); - if (input.tokenPrice * input.soldAmount < input.requiredFunds + div(input.requiredFunds * input.threshold, 100ULL)) + if (static_cast(qpi.invocationReward()) < output.cancellationFee) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = EAuctionError::InsufficientFunds; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); + logProcedureResult(locals.log); + return; + } + + locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.core.seller; + CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); + + // Cancellation closes the auction and records it in the same history ring as finalized auctions. + locals.currentDate = qpi.now(); + locals.auction.core.status = EAuctionStatus::Cancelled; + locals.auction.core.settledAt = locals.currentDate; + locals.auction.core.allocatedQuantity = 0; + locals.auction.core.highestBidAmount = 0; + locals.auction.core.highestBidPrice = 0; + locals.auction.core.highestBidQuantity = 0; + locals.auction.core.highestBidder = NULL_ID; + locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; + state.mut().totalCancelledAuctions = sadd(state.get().totalCancelledAuctions, 1ULL); + locals.archiveClosedAuctionInput.auction = locals.auction; + CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); + + // Cancellation fees use the same epoch pool as creation and small-bid service fees. + locals.accumulateAuctionServiceFeeInput.feeAmount = output.cancellationFee; + CALL(AccumulateAuctionServiceFee, locals.accumulateAuctionServiceFeeInput, locals.accumulateAuctionServiceFeeOutput); + + if (static_cast(qpi.invocationReward()) > output.cancellationFee) + { + qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - output.cancellationFee); + } + + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, output.cancellationFee); + logProcedureResult(locals.log); + } + + /** + * @brief Lets the seller accept or reject a pending standard auction whose highest bid stayed below the sale price. + * @note The manual decision window lasts one week; after expiry the contract finalizes the sale automatically in favor of the buyer. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(ResolvePendingStandardAuction) + { + output.refundedAmount = 0; + output.errorCode = EAuctionError::InvalidInput; + + // This procedure does not need a reward; return any supplied amount before validation. + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); + if (locals.isAuctionInteractionPausedOutput.isPaused) + { + output.errorCode = EAuctionError::AuctionPaused; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); + logProcedureResult(locals.log); + return; + } + + if (input.acceptSale > 1) + { + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); + logProcedureResult(locals.log); + return; + } + + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + locals.findAuctionInput.auctionIndex = input.auctionIndex; + CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); + output.errorCode = locals.findAuctionOutput.found ? EAuctionError::AuctionClosed : EAuctionError::AuctionNotFound; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); + logProcedureResult(locals.log); + return; + } + + if (locals.auction.core.seller != qpi.invocator()) + { + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); + logProcedureResult(locals.log); + return; + } + + if (locals.auction.core.type != EAuctionType::Standard || locals.auction.core.status != EAuctionStatus::PendingSellerDecision) + { + output.errorCode = EAuctionError::AuctionClosed; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); + logProcedureResult(locals.log); + return; + } + + // If the decision window has expired, the automatic sale wins over the seller action. + locals.currentDate = qpi.now(); + if (!state.get().isAuctionTimerPaused && locals.auction.core.sellerDecisionDeadline <= locals.currentDate) + { + locals.finalizeStandardAuctionInput.auctionIndex = input.auctionIndex; + locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); + + output.errorCode = EAuctionError::AuctionClosed; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); + logProcedureResult(locals.log); + return; + } + + // Accepting finalizes the sale; rejecting refunds the bidder and returns the lot to the seller. + if (input.acceptSale) + { + locals.finalizeStandardAuctionInput.auctionIndex = input.auctionIndex; + locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); + output.errorCode = locals.finalizeStandardAuctionOutput.success ? EAuctionError::Success : EAuctionError::AuctionClosed; + } + else + { + locals.rejectStandardAuctionInput.auctionIndex = input.auctionIndex; + locals.rejectStandardAuctionInput.currentDate = locals.currentDate; + CALL(RejectStandardAuction, locals.rejectStandardAuctionInput, locals.rejectStandardAuctionOutput); + output.refundedAmount = locals.rejectStandardAuctionOutput.refundedAmount; + output.errorCode = locals.rejectStandardAuctionOutput.success ? EAuctionError::Success : EAuctionError::AuctionClosed; + } + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); + logProcedureResult(locals.log); + } + + /** + * @brief Overwrites the full auction fee configuration. + * @note Only the configured takeover coordinator can call this procedure. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(SetAuctionFees) + { + output.errorCode = EAuctionError::InvalidInput; + // Administrative procedures never consume invocation rewards. + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.get().takeoverCoordinator) + { + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFees, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + // Validate all fee tiers together so no gross-proceeds tier can exceed 100 percent. + if (!isValidAuctionFeeConfiguration(input.privateAuctionFee, input.publicAuctionCreationFee, input.auctionCancellationFeeBasisPoints, + input.managementFeeBasisPoints, input.developmentFeeBasisPoints, input.takeoverCoordinatorFeeBasisPoints, + input.shareholderDividendBasisPoints, input.shareholderFeeBasisPointsTier1, + input.shareholderFeeBasisPointsTier2, input.shareholderFeeBasisPointsTier3, + input.shareholderFeeBasisPointsTier4)) + { + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFees, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + state.mut().privateAuctionFee = input.privateAuctionFee; + state.mut().publicAuctionCreationFee = input.publicAuctionCreationFee; + state.mut().auctionCancellationFeeBasisPoints = input.auctionCancellationFeeBasisPoints; + state.mut().managementFeeBasisPoints = input.managementFeeBasisPoints; + state.mut().developmentFeeBasisPoints = input.developmentFeeBasisPoints; + state.mut().takeoverCoordinatorFeeBasisPoints = input.takeoverCoordinatorFeeBasisPoints; + state.mut().shareholderDividendBasisPoints = input.shareholderDividendBasisPoints; + state.mut().shareholderFeeBasisPointsTier1 = input.shareholderFeeBasisPointsTier1; + state.mut().shareholderFeeBasisPointsTier2 = input.shareholderFeeBasisPointsTier2; + state.mut().shareholderFeeBasisPointsTier3 = input.shareholderFeeBasisPointsTier3; + state.mut().shareholderFeeBasisPointsTier4 = input.shareholderFeeBasisPointsTier4; + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFees, output.errorCode, 0, 0); + logProcedureResult(locals.log); + } + + /** + * @brief Updates every auction fee except the takeover coordinator-specific splits. + * @note Only the configured management wallet can call this procedure. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(SetAuctionFeesByManagement) + { + output.errorCode = EAuctionError::InvalidInput; + + // Management can update operational fees, but takeover-specific fee parameters stay unchanged. + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.get().management) + { + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFeesByManagement, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + if (!isValidAuctionFeeConfiguration(input.privateAuctionFee, input.publicAuctionCreationFee, input.auctionCancellationFeeBasisPoints, + input.managementFeeBasisPoints, input.developmentFeeBasisPoints, + state.get().takeoverCoordinatorFeeBasisPoints, state.get().shareholderDividendBasisPoints, + input.shareholderFeeBasisPointsTier1, input.shareholderFeeBasisPointsTier2, + input.shareholderFeeBasisPointsTier3, input.shareholderFeeBasisPointsTier4)) + { + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFeesByManagement, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + state.mut().privateAuctionFee = input.privateAuctionFee; + state.mut().publicAuctionCreationFee = input.publicAuctionCreationFee; + state.mut().auctionCancellationFeeBasisPoints = input.auctionCancellationFeeBasisPoints; + state.mut().managementFeeBasisPoints = input.managementFeeBasisPoints; + state.mut().developmentFeeBasisPoints = input.developmentFeeBasisPoints; + state.mut().shareholderFeeBasisPointsTier1 = input.shareholderFeeBasisPointsTier1; + state.mut().shareholderFeeBasisPointsTier2 = input.shareholderFeeBasisPointsTier2; + state.mut().shareholderFeeBasisPointsTier3 = input.shareholderFeeBasisPointsTier3; + state.mut().shareholderFeeBasisPointsTier4 = input.shareholderFeeBasisPointsTier4; + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFeesByManagement, output.errorCode, 0, 0); + logProcedureResult(locals.log); + } + + /** + * @brief Reassigns the management role to another wallet. + * @note Only the configured takeover coordinator can call this procedure. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(SetManagement) + { + output.errorCode = EAuctionError::InvalidInput; + + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.get().takeoverCoordinator) + { + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetManagement, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + if (isZero(input.management)) + { + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetManagement, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + state.mut().management = input.management; + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetManagement, output.errorCode, 0, 0); + logProcedureResult(locals.log); + } + + /** + * @brief Configures the execution fee reserve guard that triggers an emergency pause on a sudden reserve drop. + * @note Only the configured takeover coordinator or management wallet can call this procedure. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(SetFeeReserveGuardConfig) + { + output.errorCode = EAuctionError::InvalidInput; + // Resetting the baseline forces the guard to start a fresh observation window. + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.get().takeoverCoordinator && qpi.invocator() != state.get().management) + { + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetFeeReserveGuardConfig, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + if (input.dropBasisPoints == 0 || input.dropBasisPoints > NOST_BASIS_POINTS_SCALE || input.windowSeconds == 0) + { + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetFeeReserveGuardConfig, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + state.mut().feeReserveGuardDropBasisPoints = input.dropBasisPoints; + state.mut().feeReserveGuardWindowSeconds = input.windowSeconds; + state.mut().feeReserveBaselineAt.setInvalid(); + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetFeeReserveGuardConfig, output.errorCode, 0, 0); + logProcedureResult(locals.log); + } + + /** + * @brief Manually pauses or resumes every auction interaction, overriding the automatic execution fee reserve guard. + * @note Only the configured takeover coordinator or management wallet can call this procedure. Resuming clears the guard window so a stale + * baseline cannot immediately retrigger the pause. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(SetEmergencyPause) + { + output.errorCode = EAuctionError::InvalidInput; + // Manual pause shares the same state as the automatic reserve guard. + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.get().takeoverCoordinator && qpi.invocator() != state.get().management) + { + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetEmergencyPause, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; } - if (qpi.invocationReward() < NOSTROMO_QX_TOKEN_ISSUANCE_FEE) + if (input.paused) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; + state.mut().isEmergencyPaused = 1; + state.mut().emergencyPausedAt = qpi.now(); } - - if (qpi.invocationReward() > NOSTROMO_QX_TOKEN_ISSUANCE_FEE) + else { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - NOSTROMO_QX_TOKEN_ISSUANCE_FEE); + state.mut().isEmergencyPaused = 0; + state.mut().emergencyPausedAt.setInvalid(); + state.mut().feeReserveBaselineAt.setInvalid(); } - locals.tmpProject = state.get().projects.get(input.indexOfProject); - locals.tmpProject.isCreatedFundarasing = 1; - state.mut().projects.set(input.indexOfProject, locals.tmpProject); - - locals.newFundraising.tokenPrice = input.tokenPrice; - locals.newFundraising.soldAmount = input.soldAmount; - locals.newFundraising.requiredFunds = input.requiredFunds; - locals.newFundraising.raisedFunds = 0; - locals.newFundraising.indexOfProject = input.indexOfProject; - locals.newFundraising.firstPhaseStartDate = locals.firstPhaseStartDate; - locals.newFundraising.firstPhaseEndDate = locals.firstPhaseEndDate; - locals.newFundraising.secondPhaseStartDate = locals.secondPhaseStartDate; - locals.newFundraising.secondPhaseEndDate = locals.secondPhaseEndDate; - locals.newFundraising.thirdPhaseStartDate = locals.thirdPhaseStartDate; - locals.newFundraising.thirdPhaseEndDate = locals.thirdPhaseEndDate; - locals.newFundraising.listingStartDate = locals.listingStartDate; - locals.newFundraising.cliffEndDate = locals.cliffEndDate; - locals.newFundraising.vestingEndDate = locals.vestingEndDate; - locals.newFundraising.threshold = input.threshold; - locals.newFundraising.TGE = input.TGE; - locals.newFundraising.stepOfVesting = input.stepOfVesting; - - state.mut().fundaraisings.set(state.get().numberOfFundraising, locals.newFundraising); - state.mut().numberOfFundraising++; + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetEmergencyPause, output.errorCode, 0, 0); + logProcedureResult(locals.log); } - struct investInProject_locals - { - QX::IssueAsset_input input; - QX::IssueAsset_output output; - QX::TransferShareManagementRights_input TransferShareManagementRightsInput; - QX::TransferShareManagementRights_output TransferShareManagementRightsOutput; - investInfo tmpInvestData; - fundaraisingInfo tmpFundraising; - uint64 maxCap, minCap, maxInvestmentPerUser, userInvestedAmount; - uint32 curDate, elementIndex, i, numberOfInvestedProjects; - uint8 tierLevel; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(investInProject) + /** + * @brief Returns the stored state of one auction. + * @note The response contains a serializable auction view; access-control containers are returned as fixed arrays with counts. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionByIndex) { - if (input.indexOfFundraising >= state.get().numberOfFundraising || qpi.invocationReward() == 0) + output.found = 0; + locals.findAuctionInput.auctionIndex = input.auctionIndex; + CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); + if (!locals.findAuctionOutput.found) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; + return; } + locals.auction = locals.findAuctionOutput.auction; - locals.maxCap = state.get().fundaraisings.get(input.indexOfFundraising).requiredFunds + div(state.get().fundaraisings.get(input.indexOfFundraising).requiredFunds * state.get().fundaraisings.get(input.indexOfFundraising).threshold, 100ULL); - locals.minCap = state.get().fundaraisings.get(input.indexOfFundraising).requiredFunds - div(state.get().fundaraisings.get(input.indexOfFundraising).requiredFunds * state.get().fundaraisings.get(input.indexOfFundraising).threshold, 100ULL); - if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects) && locals.numberOfInvestedProjects >= NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST) + output.found = 1; + output.auction.core = locals.auction.core; + + // Hash containers are flattened into arrays because they are not part of the public ABI surface. + output.auction.requiredAccessAssetCount = 0; + for (locals.requiredAccessAssetSetIndex = locals.auction.requiredAccessAssets.nextElementIndex(NULL_INDEX); + locals.requiredAccessAssetSetIndex != NULL_INDEX; + locals.requiredAccessAssetSetIndex = locals.auction.requiredAccessAssets.nextElementIndex(locals.requiredAccessAssetSetIndex)) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; + locals.requiredAccessAsset.asset = locals.auction.requiredAccessAssets.key(locals.requiredAccessAssetSetIndex); + locals.requiredAccessAsset.quantity = locals.auction.requiredAccessAssets.value(locals.requiredAccessAssetSetIndex); + output.auction.requiredAccessAssets.set(output.auction.requiredAccessAssetCount, locals.requiredAccessAsset); + output.auction.requiredAccessAssetCount = sadd(output.auction.requiredAccessAssetCount, 1ULL); } - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); - - locals.tmpFundraising = state.get().fundaraisings.get(input.indexOfFundraising); - - if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).firstPhaseStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).firstPhaseEndDate) + output.auction.allowedBidderWalletCount = 0; + for (locals.allowedBidderWalletSetIndex = locals.auction.allowedBidderWallets.nextElementIndex(NULL_INDEX); + locals.allowedBidderWalletSetIndex != NULL_INDEX; + locals.allowedBidderWalletSetIndex = locals.auction.allowedBidderWallets.nextElementIndex(locals.allowedBidderWalletSetIndex)) { - if (state.get().users.contains(qpi.invocator()) == 0) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - - state.get().users.get(qpi.invocator(), locals.tierLevel); - switch (locals.tierLevel) - { - case 1: - locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT, state.get().totalPoolWeight); - break; - case 2: - locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT, state.get().totalPoolWeight); - break; - case 3: - locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_DOG_POOL_WEIGHT, state.get().totalPoolWeight); - break; - case 4: - locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT, state.get().totalPoolWeight); - break; - case 5: - locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_WARRIOR_POOL_WEIGHT, state.get().totalPoolWeight); - break; - default: - break; - } - - state.get().investors.get(qpi.invocator(), state.mut().tmpInvestedList); - state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects); - - for (locals.i = 0; locals.i < locals.numberOfInvestedProjects; locals.i++) - { - if (state.get().tmpInvestedList.get(locals.i).indexOfFundraising == input.indexOfFundraising) - { - locals.userInvestedAmount = state.get().tmpInvestedList.get(locals.i).investedAmount; - break; - } - } - - locals.tmpInvestData.indexOfFundraising = input.indexOfFundraising; - - if (locals.i < locals.numberOfInvestedProjects) - { - if (locals.tmpFundraising.raisedFunds + locals.maxInvestmentPerUser - locals.userInvestedAmount > locals.maxCap) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - if (qpi.invocationReward() + locals.userInvestedAmount > locals.maxInvestmentPerUser) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() + locals.userInvestedAmount - locals.maxInvestmentPerUser); - - locals.tmpInvestData.investedAmount = locals.maxInvestmentPerUser; - locals.tmpFundraising.raisedFunds += locals.maxInvestmentPerUser - locals.userInvestedAmount; - } - else - { - locals.tmpInvestData.investedAmount = qpi.invocationReward() + locals.userInvestedAmount; - locals.tmpFundraising.raisedFunds += qpi.invocationReward(); - } - state.mut().tmpInvestedList.set(locals.i, locals.tmpInvestData); - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - } - else - { - if (locals.tmpFundraising.raisedFunds + locals.maxInvestmentPerUser > locals.maxCap) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - if (qpi.invocationReward() > (sint64)locals.maxInvestmentPerUser) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.maxInvestmentPerUser); - locals.tmpInvestData.investedAmount = locals.maxInvestmentPerUser; - locals.tmpFundraising.raisedFunds += locals.maxInvestmentPerUser; - } - else - { - locals.tmpInvestData.investedAmount = qpi.invocationReward(); - locals.tmpFundraising.raisedFunds += qpi.invocationReward(); - } - - state.mut().tmpInvestedList.set(locals.numberOfInvestedProjects, locals.tmpInvestData); - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects)) - { - state.mut().numberOfInvestedProjects.set(qpi.invocator(), locals.numberOfInvestedProjects + 1); - } - else - { - state.mut().numberOfInvestedProjects.set(qpi.invocator(), 1); - } - } + locals.allowedBidderWallet = locals.auction.allowedBidderWallets.key(locals.allowedBidderWalletSetIndex); + output.auction.allowedBidderWallets.set(output.auction.allowedBidderWalletCount, locals.allowedBidderWallet); + output.auction.allowedBidderWalletCount = sadd(output.auction.allowedBidderWalletCount, 1ULL); } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).secondPhaseStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).secondPhaseEndDate) - { - if (state.get().users.contains(qpi.invocator()) == 0) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - - state.get().users.get(qpi.invocator(), locals.tierLevel); - if (locals.tierLevel < 4) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - switch (locals.tierLevel) - { - case 4: - locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT, state.get().totalPoolWeight); - break; - case 5: - locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_WARRIOR_POOL_WEIGHT, state.get().totalPoolWeight); - break; - default: - break; - } - - state.get().investors.get(qpi.invocator(), state.mut().tmpInvestedList); - state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects); + } - for (locals.i = 0; locals.i < locals.numberOfInvestedProjects; locals.i++) + /** + * @brief Returns the stored bid state of one wallet in one auction. + * @note The response indicates whether a participant record exists for the requested auction and wallet. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionParticipant) + { + output.found = 0; + locals.bestParticipantFound = 0; + // A wallet can have multiple historical batch bid slots; return the newest matching record. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) + { + locals.participantData = state.get().participants.get(locals.participantSlotIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex || + locals.participantData.participant != input.participant) { - if (state.get().tmpInvestedList.get(locals.i).indexOfFundraising == input.indexOfFundraising) - { - locals.userInvestedAmount = state.get().tmpInvestedList.get(locals.i).investedAmount; - break; - } + continue; } - locals.tmpInvestData.indexOfFundraising = input.indexOfFundraising; - - if (locals.i < locals.numberOfInvestedProjects) - { - if (locals.tmpFundraising.raisedFunds + locals.maxInvestmentPerUser - locals.userInvestedAmount > locals.maxCap) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - if (qpi.invocationReward() + locals.userInvestedAmount > locals.maxInvestmentPerUser) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() + locals.userInvestedAmount - locals.maxInvestmentPerUser); - - locals.tmpInvestData.investedAmount = locals.maxInvestmentPerUser; - locals.tmpFundraising.raisedFunds += locals.maxInvestmentPerUser - locals.userInvestedAmount; - } - else - { - locals.tmpInvestData.investedAmount = qpi.invocationReward() + locals.userInvestedAmount; - locals.tmpFundraising.raisedFunds += qpi.invocationReward(); - } - state.mut().tmpInvestedList.set(locals.i, locals.tmpInvestData); - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - } - else + if (!locals.bestParticipantFound || locals.participantData.bidIndex > output.participantData.bidIndex) { - if (locals.tmpFundraising.raisedFunds + locals.maxInvestmentPerUser > locals.maxCap) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - if (qpi.invocationReward() > (sint64)locals.maxInvestmentPerUser) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.maxInvestmentPerUser); - locals.tmpInvestData.investedAmount = locals.maxInvestmentPerUser; - locals.tmpFundraising.raisedFunds += locals.maxInvestmentPerUser; - } - else - { - locals.tmpInvestData.investedAmount = qpi.invocationReward(); - locals.tmpFundraising.raisedFunds += qpi.invocationReward(); - } - - state.mut().tmpInvestedList.set(locals.numberOfInvestedProjects, locals.tmpInvestData); - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects)) - { - state.mut().numberOfInvestedProjects.set(qpi.invocator(), locals.numberOfInvestedProjects + 1); - } - else - { - state.mut().numberOfInvestedProjects.set(qpi.invocator(), 1); - } + locals.bestParticipantFound = 1; + locals.bestParticipantSlotIndex = locals.participantSlotIndex; + output.participantData = locals.participantData; + output.found = 1; } } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).thirdPhaseStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).thirdPhaseEndDate) + // Search archived slots as well because displaced and settled bids are removed from the live array. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participantHistory.capacity(); ++locals.participantSlotIndex) { - if (locals.tmpFundraising.raisedFunds + qpi.invocationReward() > locals.maxCap) + locals.participantData = state.get().participantHistory.get(locals.participantSlotIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex || + locals.participantData.participant != input.participant) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; + continue; } - state.get().investors.get(qpi.invocator(), state.mut().tmpInvestedList); - state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects); - - for (locals.i = 0; locals.i < locals.numberOfInvestedProjects; locals.i++) + if (!locals.bestParticipantFound || locals.participantData.bidIndex > output.participantData.bidIndex) { - if (state.get().tmpInvestedList.get(locals.i).indexOfFundraising == input.indexOfFundraising) - { - locals.userInvestedAmount = state.get().tmpInvestedList.get(locals.i).investedAmount; - break; - } + locals.bestParticipantFound = 1; + output.participantData = locals.participantData; + output.found = 1; } + } + } - locals.tmpInvestData.indexOfFundraising = input.indexOfFundraising; - - if (locals.i < locals.numberOfInvestedProjects) - { - locals.tmpInvestData.investedAmount = qpi.invocationReward() + locals.userInvestedAmount; - state.mut().tmpInvestedList.set(locals.i, locals.tmpInvestData); - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - } - else - { - locals.tmpInvestData.investedAmount = qpi.invocationReward(); + /** + * @brief Returns the remaining post-BEGIN_EPOCH pause before auction interactions resume. + * @note This getter exposes the 500-tick launch pause referenced by the auction timing rules. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetTicksBeforeAuctionLaunch) + { + output.ticks = 0; - state.mut().tmpInvestedList.set(locals.numberOfInvestedProjects, locals.tmpInvestData); - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects)) - { - state.mut().numberOfInvestedProjects.set(qpi.invocator(), locals.numberOfInvestedProjects + 1); - } - else - { - state.mut().numberOfInvestedProjects.set(qpi.invocator(), 1); - } - } - locals.tmpFundraising.raisedFunds += qpi.invocationReward(); - } - else + if (!state.get().isPostBeginEpochPauseArmed) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; + return; } - if (locals.minCap <= locals.tmpFundraising.raisedFunds && locals.tmpFundraising.isCreatedToken == 0) - { - locals.input.assetName = state.get().projects.get(locals.tmpFundraising.indexOfProject).tokenName; - locals.input.numberOfDecimalPlaces = 0; - locals.input.numberOfShares = state.get().projects.get(locals.tmpFundraising.indexOfProject).supplyOfToken; - locals.input.unitOfMeasurement = 0; - INVOKE_OTHER_CONTRACT_PROCEDURE(QX, IssueAsset, locals.input, locals.output, NOSTROMO_QX_TOKEN_ISSUANCE_FEE); + output.ticks = static_cast(max(static_cast(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) - + (static_cast(qpi.tick()) - static_cast(qpi.initialTick())), + 0)); + } - if (locals.output.issuedNumberOfShares == state.get().projects.get(locals.tmpFundraising.indexOfProject).supplyOfToken) - { - locals.tmpFundraising.isCreatedToken = 1; + /** + * @brief Returns the current auction fee configuration stored in contract state. + * @note The response includes creation, cancellation, revenue split, and tier-based shareholder fee parameters. + */ + PUBLIC_FUNCTION(GetAuctionFees) + { + output.privateAuctionFee = state.get().privateAuctionFee; + output.auctionCancellationFeeBasisPoints = state.get().auctionCancellationFeeBasisPoints; + output.managementFeeBasisPoints = state.get().managementFeeBasisPoints; + output.developmentFeeBasisPoints = state.get().developmentFeeBasisPoints; + output.takeoverCoordinatorFeeBasisPoints = state.get().takeoverCoordinatorFeeBasisPoints; + output.shareholderDividendBasisPoints = state.get().shareholderDividendBasisPoints; + output.shareholderFeeBasisPointsTier1 = state.get().shareholderFeeBasisPointsTier1; + output.shareholderFeeBasisPointsTier2 = state.get().shareholderFeeBasisPointsTier2; + output.shareholderFeeBasisPointsTier3 = state.get().shareholderFeeBasisPointsTier3; + output.shareholderFeeBasisPointsTier4 = state.get().shareholderFeeBasisPointsTier4; + output.publicAuctionCreationFee = state.get().publicAuctionCreationFee; + } - locals.TransferShareManagementRightsInput.asset.assetName = state.get().projects.get(locals.tmpFundraising.indexOfProject).tokenName; - locals.TransferShareManagementRightsInput.asset.issuer = SELF; - locals.TransferShareManagementRightsInput.newManagingContractIndex = SELF_INDEX; - locals.TransferShareManagementRightsInput.numberOfShares = state.get().projects.get(locals.tmpFundraising.indexOfProject).supplyOfToken; + /** + * @brief Calculates the escrow, accumulated fee, and reward required by Batch Auction bid arithmetic. + * @param input Prospective bid quantity and price per asset; zero values are accepted for arithmetic inspection. + * @param output Saturating escrow product, small-bid fee, and saturating total reward. + * @note Non-zero escrow pays enough fee to reach `NOST_BATCH_BID_FEE_CUTOFF`; escrow at or above the cutoff pays no bid fee. + * @note This function does not validate whether `PlaceBid` would accept the bid or mutate contract state. + */ + PUBLIC_FUNCTION(CalculateBatchAuctionBidFee) { calculateBatchAuctionBidFee(input.bidQuantity, input.bidAmount, output); } - INVOKE_OTHER_CONTRACT_PROCEDURE(QX, TransferShareManagementRights, locals.TransferShareManagementRightsInput, locals.TransferShareManagementRightsOutput, 0); + /** + * @brief Returns the current wallets that receive auction fee transfers. + * @note The response exposes the configured management, development, and takeover coordinator addresses. + */ + PUBLIC_FUNCTION(GetFeeRecipients) + { + output.management = state.get().management; + output.development = state.get().development; + output.takeoverCoordinator = state.get().takeoverCoordinator; + } - qpi.transferShareOwnershipAndPossession(state.get().projects.get(locals.tmpFundraising.indexOfProject).tokenName, SELF, SELF, SELF, state.get().projects.get(locals.tmpFundraising.indexOfProject).supplyOfToken - locals.tmpFundraising.soldAmount, state.get().projects.get(locals.tmpFundraising.indexOfProject).creator); + /** + * @brief Returns the ring buffer with recently closed auctions. + * @note The buffer stores auction identifiers for both finalized and cancelled auctions. + * @note When `totalEntries` exceeds `NOST_AUCTION_HISTORY_NUM`, older entries are overwritten in ring-buffer order. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetClosedAuctionHistory) + { + // Preserve physical ring positions to keep the existing auctionIndices ABI stable for clients. + for (locals.historyIndex = 0; locals.historyIndex < state.get().closedAuctionHistory.capacity(); ++locals.historyIndex) + { + locals.auction = state.get().closedAuctionHistory.get(locals.historyIndex); + if (locals.auction.core.status != EAuctionStatus::None) + { + output.auctionIndices.set(locals.historyIndex, locals.auction.core.auctionIndex); } } + output.totalEntries = state.get().closedAuctionHistoryCounter; + } - state.mut().fundaraisings.set(input.indexOfFundraising, locals.tmpFundraising); + /** + * @brief Returns whether the temporary fee override routes every fee to development. + */ + PUBLIC_FUNCTION(GetRouteAllFeesToDevelopment) { output.enabled = state.get().routeAllFeesToDevelopment; } + + /** + * @brief Returns the aggregate shared fee amount awaiting `END_EPOCH` settlement. + * @note The legacy field name is retained for ABI compatibility. + */ + PUBLIC_FUNCTION(GetPendingServiceFeePool) { output.pendingServiceFeePool = getNostromoFeePoolTotal(state.get().feePool); } + /** @brief Returns every accumulator in the shared Nostromo fee pool. */ + PUBLIC_FUNCTION(GetNostromoFeePool) + { + output.feePool = state.get().feePool; + output.totalAmount = getNostromoFeePoolTotal(state.get().feePool); } - struct claimToken_locals + /** @brief Returns the QU obligation currently registered for one wallet. */ + PUBLIC_FUNCTION(GetPendingPayout) { - investInfo tmpInvestData; - uint64 maxClaimAmount, investedAmount, dayA, dayB, start_cur_diffSecond, cur_end_diffSecond, claimedAmount; - uint32 curDate, tmpDate, numberOfInvestedProjects; - sint32 i, j; - uint8 curVestingStep, vestingPercent; - }; + output.amount = 0; + state.get().pendingQuPayouts.get(input.account, output.amount); + } - PUBLIC_PROCEDURE_WITH_LOCALS(claimToken) + /** + * @brief Returns the current state of the execution fee reserve guard, including a live reserve reading. + */ + PUBLIC_FUNCTION(GetFeeReserveGuardState) { - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); + output.currentFeeReserve = qpi.queryFeeReserve(SELF_INDEX); + output.feeReserveBaseline = state.get().feeReserveBaseline; + output.feeReserveBaselineAt = state.get().feeReserveBaselineAt; + output.emergencyPausedAt = state.get().emergencyPausedAt; + output.dropBasisPoints = state.get().feeReserveGuardDropBasisPoints; + output.windowSeconds = state.get().feeReserveGuardWindowSeconds; + output.isEmergencyPaused = state.get().isEmergencyPaused; + } - if (input.indexOfFundraising >= state.get().numberOfFundraising) + /** + * @brief Returns aggregate auction, participant, fee, and pause counters. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetContractStats) + { + output.stats.totalAuctionsCreated = state.get().totalAuctionsCreated; + output.stats.closedAuctionHistoryCounter = state.get().closedAuctionHistoryCounter; + output.stats.auctionShareholderDividendPool = state.get().auctionShareholderDividendPool; + output.stats.pendingServiceFeePool = getNostromoFeePoolTotal(state.get().feePool); + output.stats.totalPendingQuPayouts = state.get().totalPendingQuPayouts; + output.stats.retainedClosedAuctionCount = min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); + output.stats.retainedParticipantHistoryCount = min(state.get().participantHistoryCounter, state.get().participantHistory.capacity()); + output.stats.finalizedAuctionCount = state.get().totalFinalizedAuctions; + output.stats.cancelledAuctionCount = state.get().totalCancelledAuctions; + output.stats.qxTransferFee = state.get().qxTransferFee; + output.stats.routeAllFeesToDevelopment = state.get().routeAllFeesToDevelopment; + output.stats.isAuctionTimerPaused = state.get().isAuctionTimerPaused; + output.stats.isPostBeginEpochPauseArmed = state.get().isPostBeginEpochPauseArmed; + output.stats.isEmergencyPaused = state.get().isEmergencyPaused; + + // Stats scan fixed storage because participant slots and auction records are not separately indexed by status. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) { - return ; + locals.participantData = state.get().participants.get(locals.participantSlotIndex); + if (locals.participantData.isUsed) + { + output.stats.participantCount = sadd(output.stats.participantCount, 1ULL); + } } - state.get().investors.get(qpi.invocator(), state.mut().tmpInvestedList); - if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects) == 0) + for (locals.auctionElementIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); locals.auctionElementIndex != NULL_INDEX; + locals.auctionElementIndex = state.get().auctionList.nextElementIndex(locals.auctionElementIndex)) { - return ; + locals.auction = state.get().auctionList.value(locals.auctionElementIndex); + switch (locals.auction.core.status) + { + case EAuctionStatus::Active: output.stats.activeAuctionCount = sadd(output.stats.activeAuctionCount, 1ULL); break; + case EAuctionStatus::PendingSellerDecision: + output.stats.pendingSellerDecisionAuctionCount = sadd(output.stats.pendingSellerDecisionAuctionCount, 1ULL); + break; + default: break; + } } + } - for (locals.i = 0; locals.i < (sint32)locals.numberOfInvestedProjects; locals.i++) + /** + * @brief Returns a page of auction summaries ordered by creation index. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionSummaries) + { + // Live and archived records are disjoint, so the retained total does not require an ordered scan. + output.totalCount = + sadd(state.get().auctionList.population(), min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity())); + output.returnedCount = 0; + locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + if (locals.boundedLimit == 0 || input.offset >= output.totalCount) + { + return; + } + locals.scannedAuctionCount = 0; + locals.selectNextAuctionInput.hasAfterAuctionIndex = 0; + locals.selectNextAuctionInput.includeClosedAuctions = 1; + locals.selectNextAuctionInput.filterBySeller = 0; + // Cursor selection reconstructs creation order across unordered live storage and the closed-history ring. + while (output.returnedCount < locals.boundedLimit && locals.scannedAuctionCount < output.totalCount) { - if (state.get().tmpInvestedList.get(locals.i).indexOfFundraising == input.indexOfFundraising) + CALL(SelectNextRetainedAuction, locals.selectNextAuctionInput, locals.selectNextAuctionOutput); + if (!locals.selectNextAuctionOutput.found) { - locals.investedAmount = state.get().tmpInvestedList.get(locals.i).investedAmount; - locals.claimedAmount = state.get().tmpInvestedList.get(locals.i).claimedAmount; - locals.tmpInvestData = state.get().tmpInvestedList.get(locals.i); break; } + locals.auction = locals.selectNextAuctionOutput.auction; + // Skip only the requested prefix; the exact total is already available without scanning the remainder. + if (locals.scannedAuctionCount >= input.offset) + { + fillAuctionSummary(locals.auction, locals.auctionSummary); + output.auctions.set(output.returnedCount, locals.auctionSummary); + output.returnedCount = sadd(output.returnedCount, 1ULL); + } + locals.scannedAuctionCount = sadd(locals.scannedAuctionCount, 1ULL); + locals.selectNextAuctionInput.afterAuctionIndex = locals.auction.core.auctionIndex; + locals.selectNextAuctionInput.hasAfterAuctionIndex = 1; } + } - if (locals.i == locals.numberOfInvestedProjects) + /** + * @brief Returns a page of active or pending-seller-decision auction indices. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetActiveAuctionIndices) + { + // Invariant: terminal auctions are archived and removed, so every live-map entry is active or awaiting a seller decision. + output.totalCount = state.get().auctionList.population(); + output.returnedCount = 0; + locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + if (locals.boundedLimit == 0 || input.offset >= output.totalCount) { - return ; + return; } - if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).listingStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate) + locals.scannedAuctionCount = 0; + locals.selectNextAuctionInput.hasAfterAuctionIndex = 0; + locals.selectNextAuctionInput.includeClosedAuctions = 0; + locals.selectNextAuctionInput.filterBySeller = 0; + // Select only the requested live-map prefix and page; closed history cannot contain active auctions. + while (output.returnedCount < locals.boundedLimit && locals.scannedAuctionCount < output.totalCount) { - locals.maxClaimAmount = div(div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice) * state.get().fundaraisings.get(input.indexOfFundraising).TGE, 100ULL); + CALL(SelectNextRetainedAuction, locals.selectNextAuctionInput, locals.selectNextAuctionOutput); + if (!locals.selectNextAuctionOutput.found) + { + break; + } + locals.selectNextAuctionInput.afterAuctionIndex = locals.selectNextAuctionOutput.auction.core.auctionIndex; + locals.selectNextAuctionInput.hasAfterAuctionIndex = 1; + if (locals.scannedAuctionCount >= input.offset) + { + output.auctionIndices.set(output.returnedCount, locals.selectNextAuctionOutput.auction.core.auctionIndex); + output.returnedCount = sadd(output.returnedCount, 1ULL); + } + locals.scannedAuctionCount = sadd(locals.scannedAuctionCount, 1ULL); } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate) - { - locals.tmpDate = state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate; - diffDateInSecond(locals.tmpDate, locals.curDate, locals.j, locals.dayA, locals.dayB, locals.start_cur_diffSecond); - locals.tmpDate = state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate; - diffDateInSecond(locals.curDate, locals.tmpDate, locals.j, locals.dayA, locals.dayB, locals.cur_end_diffSecond); + } - locals.curVestingStep = (uint8)div(locals.start_cur_diffSecond, div(locals.start_cur_diffSecond + locals.cur_end_diffSecond, state.get().fundaraisings.get(input.indexOfFundraising).stepOfVesting * 1ULL)) + 1; - locals.vestingPercent = (uint8)div(100ULL - state.get().fundaraisings.get(input.indexOfFundraising).TGE, state.get().fundaraisings.get(input.indexOfFundraising).stepOfVesting * 1ULL) * locals.curVestingStep; - locals.maxClaimAmount = div(div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice) * (state.get().fundaraisings.get(input.indexOfFundraising).TGE + locals.vestingPercent), 100ULL); - } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate) + /** + * @brief Returns a page of auction summaries created by a seller. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionsBySeller) + { + output.returnedCount = 0; + locals.countAuctionsInput.seller = input.seller; + CALL(CountRetainedAuctionsBySeller, locals.countAuctionsInput, locals.countAuctionsOutput); + output.totalCount = locals.countAuctionsOutput.count; + locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + if (locals.boundedLimit == 0 || input.offset >= output.totalCount) { - locals.maxClaimAmount = div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice); + return; } - if (input.amount + locals.claimedAmount > locals.maxClaimAmount) - { - return ; - } - else + locals.scannedAuctionCount = 0; + locals.selectNextAuctionInput.seller = input.seller; + locals.selectNextAuctionInput.hasAfterAuctionIndex = 0; + locals.selectNextAuctionInput.includeClosedAuctions = 1; + locals.selectNextAuctionInput.filterBySeller = 1; + // The selector skips other sellers, so only the requested seller's prefix and page are ordered. + while (output.returnedCount < locals.boundedLimit && locals.scannedAuctionCount < output.totalCount) { - qpi.transferShareOwnershipAndPossession(state.get().projects.get(state.get().fundaraisings.get(input.indexOfFundraising).indexOfProject).tokenName, SELF, SELF, SELF, input.amount, qpi.invocator()); - if (input.amount + locals.claimedAmount == locals.maxClaimAmount && state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate <= locals.curDate) + CALL(SelectNextRetainedAuction, locals.selectNextAuctionInput, locals.selectNextAuctionOutput); + if (!locals.selectNextAuctionOutput.found) { - state.mut().tmpInvestedList.set(locals.i, state.get().tmpInvestedList.get(locals.numberOfInvestedProjects - 1)); - state.mut().numberOfInvestedProjects.set(qpi.invocator(), locals.numberOfInvestedProjects - 1); - } - else - { - locals.tmpInvestData.claimedAmount = input.amount + locals.claimedAmount; - state.mut().tmpInvestedList.set(locals.i, locals.tmpInvestData); + break; } - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects); - if (locals.numberOfInvestedProjects == 0) + locals.auction = locals.selectNextAuctionOutput.auction; + locals.selectNextAuctionInput.afterAuctionIndex = locals.auction.core.auctionIndex; + locals.selectNextAuctionInput.hasAfterAuctionIndex = 1; + if (locals.scannedAuctionCount >= input.offset) { - state.mut().investors.removeByKey(qpi.invocator()); - state.mut().numberOfInvestedProjects.removeByKey(qpi.invocator()); + fillAuctionSummary(locals.auction, locals.auctionSummary); + output.auctions.set(output.returnedCount, locals.auctionSummary); + output.returnedCount = sadd(output.returnedCount, 1ULL); } - output.claimedAmount = input.amount; + locals.scannedAuctionCount = sadd(locals.scannedAuctionCount, 1ULL); } } - struct upgradeTier_locals + /** + * @brief Looks up the first auction matching a metadata CID. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionByMetadataCid) { - uint64 deltaAmount; - uint32 i, deltaPoolWeight; - uint8 currentTierLevel; - }; + output.found = 0; + output.auctionIndex = 0; + locals.findAuctionInput.metadataIpfsCid = input.metadataIpfsCid; + CALL(FindFirstRetainedAuctionByMetadataCid, locals.findAuctionInput, locals.findAuctionOutput); + // The helper compares all retained candidates and returns the smallest matching creation index. + if (!locals.findAuctionOutput.found) + { + return; + } + output.found = 1; + output.auctionIndex = locals.findAuctionOutput.auction.core.auctionIndex; + fillAuctionSummary(locals.findAuctionOutput.auction, output.auction); + } - PUBLIC_PROCEDURE_WITH_LOCALS(upgradeTier) + /** + * @brief Returns auction summaries for a batch of requested indices. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionSummariesByIndexBatch) { - if (state.get().users.contains(qpi.invocator()) == 0) + output.returnedCount = 0; + locals.boundedLimit = min(input.count, NOST_AUCTION_GETTER_PAGE_SIZE); + // Preserve input positions so callers can correlate each requested index with its found flag. + for (locals.requestedIndex = 0; locals.requestedIndex < locals.boundedLimit; ++locals.requestedIndex) { - if (qpi.invocationReward() > 0) + locals.auctionIndex = input.auctionIndices.get(locals.requestedIndex); + locals.findAuctionInput.auctionIndex = locals.auctionIndex; + CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); + if (locals.findAuctionOutput.found) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + locals.auction = locals.findAuctionOutput.auction; + fillAuctionSummary(locals.auction, locals.auctionSummary); + output.auctions.set(locals.requestedIndex, locals.auctionSummary); + output.found.set(locals.requestedIndex, 1); + output.returnedCount = sadd(output.returnedCount, 1ULL); } - return ; } + } - state.get().users.get(qpi.invocator(), locals.currentTierLevel); - - switch (locals.currentTierLevel) - { - case 1: - locals.deltaAmount = NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT - NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT; - locals.deltaPoolWeight = NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT - NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT; - break; - case 2: - locals.deltaAmount = NOSTROMO_TIER_DOG_STAKE_AMOUNT - NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT; - locals.deltaPoolWeight = NOSTROMO_TIER_DOG_POOL_WEIGHT - NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; - break; - case 3: - locals.deltaAmount = NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT - NOSTROMO_TIER_DOG_STAKE_AMOUNT; - locals.deltaPoolWeight = NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT - NOSTROMO_TIER_DOG_POOL_WEIGHT; - break; - case 4: - locals.deltaAmount = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT - NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT; - locals.deltaPoolWeight = NOSTROMO_TIER_WARRIOR_POOL_WEIGHT - NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; - break; - default: - break; - } - if (input.newTierLevel != locals.currentTierLevel + 1 || qpi.invocationReward() < (sint64)locals.deltaAmount) + /** + * @brief Returns a page of participants for one auction. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionParticipants) + { + output.totalCount = 0; + output.returnedCount = 0; + locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + // Participant storage is global, so auction participant pages are built by scanning all slots. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) { - if (qpi.invocationReward() > 0) + locals.participantData = state.get().participants.get(locals.participantSlotIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + continue; + } + if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) + { + fillParticipantSummary(locals.participantData, locals.participantSummary); + output.participants.set(output.returnedCount, locals.participantSummary); + output.returnedCount = sadd(output.returnedCount, 1ULL); } - return ; + output.totalCount = sadd(output.totalCount, 1ULL); } - else + // Append archived records after live records so an offset spans both storage tiers deterministically. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participantHistory.capacity(); ++locals.participantSlotIndex) { - state.mut().users.set(qpi.invocator(), input.newTierLevel); - if (qpi.invocationReward() > (sint64)locals.deltaAmount) + locals.participantData = state.get().participantHistory.get(locals.participantSlotIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) + { + continue; + } + if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.deltaAmount); + fillParticipantSummary(locals.participantData, locals.participantSummary); + output.participants.set(output.returnedCount, locals.participantSummary); + output.returnedCount = sadd(output.returnedCount, 1ULL); } - state.mut().totalPoolWeight += locals.deltaPoolWeight; + output.totalCount = sadd(output.totalCount, 1ULL); } } - PUBLIC_PROCEDURE(TransferShareManagementRights) + /** + * @brief Returns a page of historical auction participations for one wallet. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetUserParticipations) { - if (qpi.invocationReward() < state.get().transferRightsFee) - { - return ; - } - - if (qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer,qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.numberOfShares) + output.totalCount = 0; + output.returnedCount = 0; + locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + // User participation history includes inactive records so settled and displaced bids remain visible. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) { - // not enough shares available - output.transferredNumberOfShares = 0; - if (qpi.invocationReward() > 0) + locals.participantData = state.get().participants.get(locals.participantSlotIndex); + if (!locals.participantData.isUsed || locals.participantData.participant != input.participant) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + continue; + } + if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) + { + fillUserParticipationSummary(locals.participantData.auctionIndex, locals.participantData, locals.userParticipationSummary); + output.participations.set(output.returnedCount, locals.userParticipationSummary); + output.returnedCount = sadd(output.returnedCount, 1ULL); } + output.totalCount = sadd(output.totalCount, 1ULL); } - else + // Continue the same page over archived bids after accounting for matching live entries. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participantHistory.capacity(); ++locals.participantSlotIndex) { - if (qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, - input.newManagingContractIndex, input.newManagingContractIndex, state.get().transferRightsFee) < 0) + locals.participantData = state.get().participantHistory.get(locals.participantSlotIndex); + if (!locals.participantData.isUsed || locals.participantData.participant != input.participant) { - // error - output.transferredNumberOfShares = 0; - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } + continue; } - else + if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) { - // success - output.transferredNumberOfShares = input.numberOfShares; - if (qpi.invocationReward() > state.get().transferRightsFee) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.get().transferRightsFee); - } + fillUserParticipationSummary(locals.participantData.auctionIndex, locals.participantData, locals.userParticipationSummary); + output.participations.set(output.returnedCount, locals.userParticipationSummary); + output.returnedCount = sadd(output.returnedCount, 1ULL); } + output.totalCount = sadd(output.totalCount, 1ULL); } } - PUBLIC_FUNCTION(getStats) + /** + * @brief Returns the most recently created auction index when one exists. + */ + PUBLIC_FUNCTION(GetLatestAuctionIndex) { - output.epochRevenue = state.get().epochRevenue; - output.numberOfCreatedProject = state.get().numberOfCreatedProject; - output.numberOfFundraising = state.get().numberOfFundraising; - output.numberOfRegister = state.get().numberOfRegister; - output.totalPoolWeight = state.get().totalPoolWeight; + output.found = state.get().totalAuctionsCreated > 0; + output.auctionIndex = output.found ? state.get().totalAuctionsCreated - 1 : 0; } - PUBLIC_FUNCTION(getTierLevelByUser) + /** + * @brief Counts auctions created by a seller. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionCountBySeller) { - state.get().users.get(input.userId, output.tierLevel); + locals.countAuctionsInput.seller = input.seller; + CALL(CountRetainedAuctionsBySeller, locals.countAuctionsInput, locals.countAuctionsOutput); + output.count = locals.countAuctionsOutput.count; } - PUBLIC_FUNCTION(getUserVoteStatus) + /** + * @brief Returns immutable creation-time fields for an auction. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionAtCreationSnapshot) { - state.get().numberOfVotedProject.get(input.userId, output.numberOfVotedProjects); - state.get().voteStatus.get(input.userId, output.projectIndexList); + output.found = 0; + locals.findAuctionInput.auctionIndex = input.auctionIndex; + CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); + if (!locals.findAuctionOutput.found) + { + return; + } + locals.auction = locals.findAuctionOutput.auction; + output.found = 1; + output.seller = locals.auction.core.seller; + output.createdAt = locals.auction.core.createdAt; + output.auctionIndex = locals.auction.core.auctionIndex; + output.quantityForSale = locals.auction.core.quantityForSale; + output.initialPrice = locals.auction.core.initialPrice; + output.salePrice = locals.auction.core.salePrice; + output.minimumBidIncrement = locals.auction.core.minimumBidIncrement; + output.buyNowPrice = locals.auction.core.buyNowPrice; + output.auctionDurationSeconds = locals.auction.core.auctionDurationSeconds; + output.type = static_cast(locals.auction.core.type); + output.visibility = static_cast(locals.auction.core.visibility); } - PUBLIC_FUNCTION(checkTokenCreatability) + /** + * @brief Returns current read-only guidance for the next valid Batch Auction bid. + * @note `found` also covers closed auctions while their snapshots remain in retained history. + * @note `PlaceBid` re-runs the same availability validation before accepting a bid. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetBatchAuctionBidAvailability) { - output.result = state.get().tokens.contains(input.tokenName); - } + locals.computeBatchBidAvailabilityInput.auctionIndex = input.auctionIndex; + locals.computeBatchBidAvailabilityInput.bidAmount = 0; + CALL(ComputeBatchBidAvailability, locals.computeBatchBidAvailabilityInput, output); + // Live auctions are fully classified by the availability helper, including non-Batch auctions. + if (output.found) + { + return; + } - PUBLIC_FUNCTION(getNumberOfInvestedProjects) - { - state.get().numberOfInvestedProjects.get(input.userId, output.numberOfInvestedProjects); + // A retained closed auction still exists for lookup purposes, but can never accept another bid. + locals.isClosedAuctionRetainedInput.auctionIndex = input.auctionIndex; + CALL(IsClosedAuctionRetained, locals.isClosedAuctionRetainedInput, locals.isClosedAuctionRetainedOutput); + output.found = locals.isClosedAuctionRetainedOutput.found; } -public: - struct getProjectByIndex_input + /** + * @brief Transfers share management rights for an asset position to another managing contract. + * @note The caller must currently possess at least the requested number of shares. + * @note The caller must send the destination contract's required transfer fee as invocation reward. This contract cannot query that + * fee before calling `releaseShares`, so callers must resolve it from `newManagingContractIndex`. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(TransferShareManagementRights) { - uint32 indexOfProject; - }; + locals.reward = qpi.invocationReward(); + locals.refundAmount = locals.reward; + locals.success = false; + output.transferredNumberOfShares = 0; + output.errorCode = EAuctionError::InvalidInput; + + // Emergency pause blocks cross-contract share release and returns the caller's fee budget. + if (state.get().isEmergencyPaused) + { + if (locals.refundAmount > 0) + { + qpi.transfer(qpi.invocator(), locals.refundAmount); + } + output.errorCode = EAuctionError::AuctionPaused; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::TransferShareManagementRights, output.errorCode, 0, + output.transferredNumberOfShares); + logProcedureResult(locals.log); + return; + } - struct getProjectByIndex_output - { - projectInfo project; - }; + // `releaseShares` consumes only the destination transfer fee; any unused reward is refunded below. + if (input.numberOfShares > 0 && qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer, qpi.invocator(), qpi.invocator(), + SELF_INDEX, SELF_INDEX) >= input.numberOfShares) + { + locals.result = qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, input.newManagingContractIndex, + input.newManagingContractIndex, locals.reward); + if (locals.result != INVALID_AMOUNT && locals.result >= 0) + { + locals.success = true; + locals.refundAmount = locals.reward - locals.result; + } + } - PUBLIC_FUNCTION(getProjectByIndex) - { - output.project = state.get().projects.get(input.indexOfProject); + if (locals.success) + { + output.transferredNumberOfShares = input.numberOfShares; + output.errorCode = EAuctionError::Success; + } + + if (locals.refundAmount > 0) + { + qpi.transfer(qpi.invocator(), locals.refundAmount); + } + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::TransferShareManagementRights, output.errorCode, 0, + output.transferredNumberOfShares); + + logProcedureResult(locals.log); } - struct getFundarasingByIndex_input +protected: + /** + * @brief Emits a procedure log as success or error based on its error code. + */ + static void logProcedureResult(const NostromoProcedureLog& log) { - uint32 indexOfFundarasing; - }; + if (log.errorCode == static_cast(EAuctionError::Success)) + { + LOG_INFO(log); + } + else + { + LOG_ERROR(log); + } + } - struct getFundarasingByIndex_output + /** + * @brief Fills the common procedure log payload. + */ + static void setProcedureLogInput(NostromoProcedureLog& log, const id& actor, EProcedureId procedure, EAuctionError errorCode, uint64 auctionIndex, + sint64 amount) { - fundaraisingInfo fundarasing; - }; + log.contractIndex = SELF_INDEX; + log.procedure = static_cast(procedure); + log.errorCode = static_cast(errorCode); + log.auctionIndex = auctionIndex; + log.actor = actor; + log.amount = amount; + log._terminator = 0; + } - PUBLIC_FUNCTION(getFundarasingByIndex) + /** + * @brief Copies persisted auction data into a compact summary. + */ + static void fillAuctionSummary(const AuctionData& auction, AuctionSummary& summary) { - output.fundarasing = state.get().fundaraisings.get(input.indexOfFundarasing); + summary.metadataIpfsCid = auction.core.metadataIpfsCid; + summary.seller = auction.core.seller; + summary.highestBidder = auction.core.highestBidder; + summary.createdAt = auction.core.createdAt; + summary.settledAt = auction.core.settledAt; + summary.auctionIndex = auction.core.auctionIndex; + summary.quantityForSale = auction.core.quantityForSale; + summary.allocatedQuantity = auction.core.allocatedQuantity; + summary.initialPrice = auction.core.initialPrice; + summary.salePrice = auction.core.salePrice; + summary.buyNowPrice = auction.core.buyNowPrice; + summary.highestBidPrice = auction.core.highestBidPrice; + summary.highestBidQuantity = auction.core.highestBidQuantity; + summary.highestBidAmount = auction.core.highestBidAmount; + summary.type = static_cast(auction.core.type); + summary.visibility = static_cast(auction.core.visibility); + summary.status = static_cast(auction.core.status); } - struct getProjectIndexListByCreator_input + /** + * @brief Copies participant storage data into an auction participant summary. + */ + static void fillParticipantSummary(const AuctionParticipantData& participantData, ParticipantSummary& summary) { - id creator; - }; + summary.participant = participantData.participant; + summary.lastBidTime = participantData.lastBidTime; + summary.bidAmount = participantData.bidAmount; + summary.escrowedAmount = participantData.escrowedAmount; + summary.requestedQuantity = participantData.requestedQuantity; + summary.allocatedQuantity = participantData.allocatedQuantity; + summary.isWinningBid = participantData.isWinningBid; + } - struct getProjectIndexListByCreator_output + /** + * @brief Copies participant storage data into a user participation summary. + */ + static void fillUserParticipationSummary(uint64 auctionIndex, const AuctionParticipantData& participantData, UserParticipationSummary& summary) { - Array indexListForProjects; - }; + summary.participant = participantData.participant; + summary.lastBidTime = participantData.lastBidTime; + summary.auctionIndex = auctionIndex; + summary.bidAmount = participantData.bidAmount; + summary.escrowedAmount = participantData.escrowedAmount; + summary.requestedQuantity = participantData.requestedQuantity; + summary.allocatedQuantity = participantData.allocatedQuantity; + summary.isWinningBid = participantData.isWinningBid; + } - struct getProjectIndexListByCreator_locals + /** + * @brief Returns the smaller of two values. + */ + template + static constexpr T min(const T& a, const T& b) { - uint32 i, countOfProject; - }; + return (a < b) ? a : b; + } + /** + * @brief Returns the larger of two values. + */ + template + static constexpr T max(const T& a, const T& b) + { + return a > b ? a : b; + } - PUBLIC_FUNCTION_WITH_LOCALS(getProjectIndexListByCreator) + /** + * @brief Resolves Batch Auction quantity invariants from creation input. + */ + static bool resolveBatchAuctionCreateParams(uint64 lotItemCount, uint64 totalEscrowQuantity, uint64 minimumPurchaseQuantity, + uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPrice) { - for (locals.i = 0; locals.i < state.get().numberOfCreatedProject; locals.i++) - { - if (state.get().projects.get(locals.i).creator == input.creator) - { - output.indexListForProjects.set(locals.countOfProject++, locals.i); - } - } - for (locals.i = locals.countOfProject; locals.i < NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST; locals.i++) + quantityForSale = 0; + resolvedMinimumPurchaseQuantity = 0; + if (lotItemCount != NOST_BATCH_AUCTION_LOT_ITEM_NUM || totalEscrowQuantity == 0 || minimumPurchaseQuantity == 0 || + minimumPurchaseQuantity > totalEscrowQuantity || buyNowPrice != 0) { - output.indexListForProjects.set(locals.i, NOSTROMO_MAX_NUMBER_PROJECT); + return false; } + quantityForSale = totalEscrowQuantity; + resolvedMinimumPurchaseQuantity = minimumPurchaseQuantity; + return true; } - struct getInfoUserInvested_input + /** + * @brief Resolves Standard Auction quantity and price invariants from creation input. + */ + static bool resolveStandardAuctionCreateParams(uint64 minimumBidIncrement, uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity, + uint64 buyNowPrice, uint64 initialPrice, uint64 salePrice) { - id investorId; - }; + quantityForSale = 0; + resolvedMinimumPurchaseQuantity = 0; + if (initialPrice < NOST_STANDARD_MIN_PRICE || salePrice < NOST_STANDARD_MIN_PRICE || minimumBidIncrement < NOST_STANDARD_MIN_BID_INCREMENT) + { + return false; + } - struct getInfoUserInvested_output - { - Array listUserInvested; - }; + if (initialPrice > salePrice) + { + return false; + } - struct getInfoUserInvested_locals - { - uint32 i, countOfProject; - }; + if (buyNowPrice > 0 && (buyNowPrice < initialPrice || buyNowPrice < salePrice)) + { - PUBLIC_FUNCTION_WITH_LOCALS(getInfoUserInvested) - { - state.get().investors.get(input.investorId, output.listUserInvested); - } + return false; + } - struct getMaxClaimAmount_input - { - id investorId; - uint32 indexOfFundraising; - }; + quantityForSale = NOST_STANDARD_AUCTION_LOT_COUNT; + resolvedMinimumPurchaseQuantity = 0; + return true; + } - struct getMaxClaimAmount_output + /** + * @brief Validates that private auctions use at least one supported access mode. + */ + constexpr static bool validatePrivateAuctionAccess(EAuctionVisibility visibility, uint64 requiredAccessAssetCount, uint64 allowedWalletCount) { - uint64 amount; - }; + return visibility != EAuctionVisibility::Private || requiredAccessAssetCount > 0 || allowedWalletCount > 0; + } - struct getMaxClaimAmount_locals + /** + * @brief Validates governance fee percentages and fixed service fees. + */ + constexpr static bool isValidAuctionFeeConfiguration(sint64 privateAuctionFee, sint64 publicAuctionCreationFee, + uint64 auctionCancellationFeeBasisPoints, uint64 managementFeeBasisPoints, + uint64 developmentFeeBasisPoints, uint64 takeoverCoordinatorFeeBasisPoints, + uint64 shareholderDividendBasisPoints, uint64 shareholderFeeBasisPointsTier1, + uint64 shareholderFeeBasisPointsTier2, uint64 shareholderFeeBasisPointsTier3, + uint64 shareholderFeeBasisPointsTier4) { - Array tmpInvestedList; - investInfo tmpInvestData; - uint64 maxClaimAmount, investedAmount, dayA, dayB, dayC, dayD, start_cur_diffSecond, cur_end_diffSecond, claimedAmount; - uint32 curDate, tmpDate, numberOfInvestedProjects; - sint32 i, j, k; - uint8 curVestingStep, vestingPercent; - bit flag; - }; + return privateAuctionFee >= 0 && publicAuctionCreationFee >= 0 && auctionCancellationFeeBasisPoints <= NOST_BASIS_POINTS_SCALE && + managementFeeBasisPoints <= NOST_BASIS_POINTS_SCALE && developmentFeeBasisPoints <= NOST_BASIS_POINTS_SCALE && + takeoverCoordinatorFeeBasisPoints <= NOST_BASIS_POINTS_SCALE && shareholderDividendBasisPoints <= NOST_BASIS_POINTS_SCALE && + shareholderFeeBasisPointsTier1 <= NOST_BASIS_POINTS_SCALE && shareholderFeeBasisPointsTier2 <= NOST_BASIS_POINTS_SCALE && + shareholderFeeBasisPointsTier3 <= NOST_BASIS_POINTS_SCALE && shareholderFeeBasisPointsTier4 <= NOST_BASIS_POINTS_SCALE && + (shareholderFeeBasisPointsTier1 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= + NOST_BASIS_POINTS_SCALE && + (shareholderFeeBasisPointsTier2 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= + NOST_BASIS_POINTS_SCALE && + (shareholderFeeBasisPointsTier3 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= + NOST_BASIS_POINTS_SCALE && + (shareholderFeeBasisPointsTier4 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= + NOST_BASIS_POINTS_SCALE; + } - PUBLIC_FUNCTION_WITH_LOCALS(getMaxClaimAmount) + /** + * @brief Selects the shareholder fee tier for a gross auction amount. + */ + static uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount, const StateData& state) { - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); - - if (input.indexOfFundraising >= state.get().numberOfFundraising) + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1) { - return ; + return state.shareholderFeeBasisPointsTier1; } - - state.get().investors.get(input.investorId, locals.tmpInvestedList); - if (state.get().numberOfInvestedProjects.get(input.investorId, locals.numberOfInvestedProjects) == 0) + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2) { - return ; + return state.shareholderFeeBasisPointsTier2; } - - for (locals.i = 0; locals.i < (sint32)locals.numberOfInvestedProjects; locals.i++) + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3) { - if (locals.tmpInvestedList.get(locals.i).indexOfFundraising == input.indexOfFundraising) - { - locals.investedAmount = locals.tmpInvestedList.get(locals.i).investedAmount; - locals.claimedAmount = locals.tmpInvestedList.get(locals.i).claimedAmount; - locals.tmpInvestData = locals.tmpInvestedList.get(locals.i); - break; - } + return state.shareholderFeeBasisPointsTier3; } + return state.shareholderFeeBasisPointsTier4; + } - if (locals.i == locals.numberOfInvestedProjects) - { - return ; - } + /** + * @brief Selects the shareholder fee tier from contract state. + */ + static uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount, const ContractState& state) + { + return getAuctionShareholderFeeBasisPoints(grossAmount, state.get()); + } - if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).listingStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate) + /** @brief Returns the zero-based shareholder fee tier selected by an auction gross amount. */ + constexpr static uint64 getAuctionShareholderFeeTierIndex(uint64 grossAmount) + { + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1) { - output.amount = div(div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice) * state.get().fundaraisings.get(input.indexOfFundraising).TGE, 100ULL); + return 0; } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate) + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2) { - locals.tmpDate = state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate; - diffDateInSecond(locals.tmpDate, locals.curDate, locals.j, locals.dayA, locals.dayB, locals.start_cur_diffSecond); - locals.tmpDate = state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate; - diffDateInSecond(locals.curDate, locals.tmpDate, locals.k, locals.dayC, locals.dayD, locals.cur_end_diffSecond); - - locals.curVestingStep = (uint8)div(locals.start_cur_diffSecond, div(locals.start_cur_diffSecond + locals.cur_end_diffSecond, state.get().fundaraisings.get(input.indexOfFundraising).stepOfVesting * 1ULL)) + 1; - locals.vestingPercent = (uint8)div(100ULL - state.get().fundaraisings.get(input.indexOfFundraising).TGE, state.get().fundaraisings.get(input.indexOfFundraising).stepOfVesting * 1ULL) * locals.curVestingStep; - output.amount = div(div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice) * (state.get().fundaraisings.get(input.indexOfFundraising).TGE + locals.vestingPercent), 100ULL); + return 1; } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate) + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3) { - output.amount = div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice); + return 2; } + return 3; } - REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + /** @brief Returns the saturating aggregate of every unsettled fee-pool accumulator. */ + static uint64 getNostromoFeePoolTotal(const NostromoFeePool& feePool) { - REGISTER_USER_FUNCTION(getStats, 1); - REGISTER_USER_FUNCTION(getTierLevelByUser, 2); - REGISTER_USER_FUNCTION(getUserVoteStatus, 3); - REGISTER_USER_FUNCTION(checkTokenCreatability, 4); - REGISTER_USER_FUNCTION(getNumberOfInvestedProjects, 5); - REGISTER_USER_FUNCTION(getProjectByIndex, 6); - REGISTER_USER_FUNCTION(getFundarasingByIndex, 7); - REGISTER_USER_FUNCTION(getProjectIndexListByCreator, 8); - REGISTER_USER_FUNCTION(getInfoUserInvested, 9); - REGISTER_USER_FUNCTION(getMaxClaimAmount, 10); - - REGISTER_USER_PROCEDURE(registerInTier, 1); - REGISTER_USER_PROCEDURE(logoutFromTier, 2); - REGISTER_USER_PROCEDURE(createProject, 3); - REGISTER_USER_PROCEDURE(voteInProject, 4); - REGISTER_USER_PROCEDURE(createFundraising, 5); - REGISTER_USER_PROCEDURE(investInProject, 6); - REGISTER_USER_PROCEDURE(claimToken, 7); - REGISTER_USER_PROCEDURE(upgradeTier, 8); - REGISTER_USER_PROCEDURE(TransferShareManagementRights, 9); + return sadd(sadd(sadd(feePool.shareholderDividendTier1Amount, feePool.shareholderDividendTier2Amount), + sadd(feePool.shareholderDividendTier3Amount, feePool.shareholderDividendTier4Amount)), + sadd(sadd(feePool.commonServiceFeeAmount, feePool.shareholderDividendAmount), + sadd(sadd(feePool.managementAmount, feePool.developmentAmount), feePool.takeoverCoordinatorAmount))); } - INITIALIZE() + /** + * @brief Computes `floor(amount * basisPoints / 10000)` without overflowing the intermediate product. + */ + static uint64 calculateBasisPointAmount(uint64 amount, uint64 basisPoints) { - state.mut().teamAddress = ID(_G, _E, _H, _N, _R, _F, _U, _O, _I, _I, _C, _S, _B, _C, _S, _R, _F, _M, _N, _J, _T, _C, _J, _K, _C, _J, _H, _A, _T, _Z, _X, _A, _X, _Y, _O, _F, _W, _X, _U, _F, _L, _C, _K, _F, _P, _B, _W, _X, _Q, _A, _C, _B, _S, _Z, _F, _F); - state.mut().transferRightsFee = 100; + return sadd(smul(div(amount, NOST_BASIS_POINTS_SCALE), basisPoints), + div(smul(mod(amount, NOST_BASIS_POINTS_SCALE), basisPoints), NOST_BASIS_POINTS_SCALE)); } - struct END_EPOCH_locals + /** + * @brief Computes the exact auction fee split without performing transfers. + * @note Keep this helper pure so tests can reuse the same arithmetic as `DistributeAuctionRevenue`. + */ + static void calculateAuctionRevenueBreakdown(uint64 grossAmount, const ContractState& state, + AuctionRevenueBreakdown& output) { - fundaraisingInfo tmpFundraising; - investInfo tmpInvest; - Array votedList; - Array clearedVotedList; - id userId; - sint64 idx; - uint32 numberOfVotedProject, clearedNumberOfVotedProject, i, j, curDate, indexOfProject, numberOfInvestedProjects, tierLevel; - }; + output.sellerPayout = grossAmount; + output.shareholderFeeBasisPoints = getAuctionShareholderFeeBasisPoints(grossAmount, state); + output.shareholderFeeAmount = calculateBasisPointAmount(grossAmount, output.shareholderFeeBasisPoints); + output.shareholderDividendAmount = calculateBasisPointAmount(output.shareholderFeeAmount, state.get().shareholderDividendBasisPoints); + output.managementFeeAmount = calculateBasisPointAmount(grossAmount, state.get().managementFeeBasisPoints); + output.developmentFeeAmount = calculateBasisPointAmount(grossAmount, state.get().developmentFeeBasisPoints); + output.takeoverCoordinatorBaseAmount = calculateBasisPointAmount(grossAmount, state.get().takeoverCoordinatorFeeBasisPoints); + output.takeoverCoordinatorFeeAmount = output.takeoverCoordinatorBaseAmount + (output.shareholderFeeAmount - output.shareholderDividendAmount); + output.sellerPayout = grossAmount - output.shareholderFeeAmount - output.managementFeeAmount - output.developmentFeeAmount - + output.takeoverCoordinatorBaseAmount; + } - END_EPOCH_WITH_LOCALS() + /** + * @brief Computes the exact service-fee split without performing transfers. + * @note Keep this helper pure so tests can reuse the same arithmetic as `DistributeNostromoFeePool`. + */ + static void calculateAuctionServiceFeeBreakdown(uint64 feeAmount, AuctionServiceFeeBreakdown& output) { - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); + output.shareholderDividendAmount = calculateBasisPointAmount(feeAmount, NOST_AUCTION_SERVICE_FEE_SHAREHOLDER_BP); + output.managementFeeAmount = calculateBasisPointAmount(feeAmount, NOST_AUCTION_SERVICE_FEE_MANAGEMENT_BP); + output.developmentFeeAmount = calculateBasisPointAmount(feeAmount, NOST_AUCTION_SERVICE_FEE_DEVELOPMENT_BP); + output.takeoverCoordinatorFeeAmount = calculateBasisPointAmount(feeAmount, NOST_AUCTION_SERVICE_FEE_TAKEOVER_COORDINATOR_BP); + // Shareholders receive the rounding remainder so the entire collected fee is distributed on-chain. + output.shareholderDividendAmount = + sadd(output.shareholderDividendAmount, feeAmount - output.shareholderDividendAmount - output.managementFeeAmount - + output.developmentFeeAmount - output.takeoverCoordinatorFeeAmount); + } - locals.idx = state.get().investors.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) + /** + * @brief Computes escrow, bid fee, and required reward for a Batch Auction bid. + */ + static void calculateBatchAuctionBidFee(uint64 bidQuantity, uint64 bidAmount, CalculateBatchAuctionBidFee_output& output) + { + output.escrowAmount = smul(bidQuantity, bidAmount); + if (output.escrowAmount == 0) { - locals.userId = state.get().investors.key(locals.idx); - state.get().investors.get(locals.userId, state.mut().tmpInvestedList); - state.get().numberOfInvestedProjects.get(locals.userId, locals.numberOfInvestedProjects); - - for (locals.i = 0; locals.i < locals.numberOfInvestedProjects; locals.i++) - { - if (state.get().fundaraisings.get(locals.i).thirdPhaseEndDate < locals.curDate && state.get().fundaraisings.get(locals.i).isCreatedToken == 0 && state.get().fundaraisings.get(locals.i).raisedFunds != 0) - { - qpi.transfer(locals.userId, state.get().tmpInvestedList.get(locals.i).investedAmount); - state.mut().tmpInvestedList.set(locals.i, state.get().tmpInvestedList.get(--locals.numberOfInvestedProjects)); - } - } - if (locals.numberOfInvestedProjects == 0) - { - state.mut().investors.removeByKey(locals.userId); - state.mut().numberOfInvestedProjects.removeByKey(locals.userId); - } - else - { - state.mut().investors.set(locals.userId, state.get().tmpInvestedList); - state.mut().numberOfInvestedProjects.set(locals.userId, locals.numberOfInvestedProjects); - } - locals.idx = state.get().investors.nextElementIndex(locals.idx); + output.fee = 0; + output.requiredReward = 0; + return; } - for (locals.i = 0; locals.i < state.get().numberOfFundraising; locals.i++) - { - if (state.get().fundaraisings.get(locals.i).thirdPhaseEndDate < locals.curDate && state.get().fundaraisings.get(locals.i).isCreatedToken == 0 && state.get().fundaraisings.get(locals.i).raisedFunds != 0) - { - locals.tmpFundraising = state.get().fundaraisings.get(locals.i); - locals.tmpFundraising.raisedFunds = 0; - state.mut().fundaraisings.set(locals.i, locals.tmpFundraising); - } - else if (state.get().fundaraisings.get(locals.i).thirdPhaseEndDate < locals.curDate && state.get().fundaraisings.get(locals.i).isCreatedToken == 1 && state.get().fundaraisings.get(locals.i).raisedFunds != 0) - { - locals.tmpFundraising = state.get().fundaraisings.get(locals.i); + output.fee = output.escrowAmount <= NOST_BATCH_BID_FEE_CUTOFF ? NOST_BATCH_BID_FEE_CUTOFF - output.escrowAmount : 0; + output.requiredReward = sadd(output.escrowAmount, output.fee); + } - state.mut().epochRevenue += div(locals.tmpFundraising.raisedFunds * 5, 100ULL); - qpi.transfer(state.get().projects.get(locals.tmpFundraising.indexOfProject).creator, locals.tmpFundraising.raisedFunds - div(locals.tmpFundraising.raisedFunds * 5, 100ULL)); + /** + * @brief Returns the service fee required to create an auction. + */ + static sint64 getCreateAuctionFee(EAuctionVisibility visibility, const ContractState& state) + { + switch (visibility) + { + case EAuctionVisibility::Public: return state.get().publicAuctionCreationFee; break; + case EAuctionVisibility::Private: return state.get().privateAuctionFee; break; + default: break; + } + return 0; + } - qpi.transferShareOwnershipAndPossession(state.get().projects.get(locals.tmpFundraising.indexOfProject).tokenName, SELF, SELF, SELF, state.get().fundaraisings.get(locals.i).soldAmount - div(locals.tmpFundraising.raisedFunds, state.get().fundaraisings.get(locals.i).tokenPrice), state.get().projects.get(locals.tmpFundraising.indexOfProject).creator); + /** + * @brief Returns whether an auction type is accepted by the contract. + */ + static bool isSupportedAuctionType(EAuctionType auctionType) + { + return auctionType == EAuctionType::Batch || auctionType == EAuctionType::Standard; + } - locals.tmpFundraising.raisedFunds = 0; - state.mut().fundaraisings.set(locals.i, locals.tmpFundraising); - } - } + /** + * @brief Returns whether an auction visibility is accepted by the contract. + */ + static bool isSupportedAuctionVisibility(EAuctionVisibility visibility) + { + return visibility == EAuctionVisibility::Public || visibility == EAuctionVisibility::Private; + } - qpi.transfer(state.get().teamAddress, div(state.get().epochRevenue, 10ULL)); - state.mut().epochRevenue -= div(state.get().epochRevenue, 10ULL); - qpi.distributeDividends(div(state.get().epochRevenue, 676ULL)); - state.mut().epochRevenue -= div(state.get().epochRevenue, 676ULL) * 676; + /** + * @brief Returns whether an asset entry is empty. + */ + static bool isZeroAsset(const Asset& asset) { return asset.assetName == 0 && isZero(asset.issuer); } - locals.idx = state.get().users.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) - { - locals.userId = state.get().users.key(locals.idx); - locals.tierLevel = state.get().users.value(locals.idx); + /** @brief Returns whether the runtime fee override routes every auction fee to the development wallet. */ + static bool routeAllFeesToDevelopment(const QPI::ContractState& state) + { + return state.get().routeAllFeesToDevelopment; + } - if (state.get().numberOfVotedProject.get(locals.userId, locals.numberOfVotedProject)) - { - state.get().voteStatus.get(locals.userId, locals.votedList); - locals.clearedNumberOfVotedProject = 0; - for (locals.j = 0; locals.j < locals.numberOfVotedProject; locals.j++) - { - locals.indexOfProject = locals.votedList.get(locals.j); + /** + * @brief Packs year, month, and day into the contract date-stamp format. + */ + static void makeDateStamp(uint8 year, uint8 month, uint8 day, uint32& res) + { + res = static_cast(year << NOST_DATE_STAMP_YEAR_SHIFT | month << NOST_DATE_STAMP_MONTH_SHIFT | day); + } - if (state.get().projects.get(locals.indexOfProject).endDate > locals.curDate) - { - locals.clearedVotedList.set(locals.clearedNumberOfVotedProject++, locals.indexOfProject); - } - } - if (locals.clearedNumberOfVotedProject == 0) - { - state.mut().numberOfVotedProject.removeByKey(locals.userId); - state.mut().voteStatus.removeByKey(locals.userId); - } - else - { - state.mut().numberOfVotedProject.set(locals.userId, locals.clearedNumberOfVotedProject); - state.mut().voteStatus.set(locals.userId, locals.clearedVotedList); - } - } + /** + * @brief Expands an accumulated pause window to include a candidate window. + */ + static void accumulatePauseWindow(uint8& hasPauseWindow, DateAndTime& pauseStartedAt, DateAndTime& pauseEndsAt, + const DateAndTime& candidatePauseStartedAt, const DateAndTime& candidatePauseEndsAt) + { + if (!hasPauseWindow) + { + hasPauseWindow = 1; + pauseStartedAt = candidatePauseStartedAt; + pauseEndsAt = candidatePauseEndsAt; + return; + } - locals.idx = state.get().users.nextElementIndex(locals.idx); + if (candidatePauseStartedAt < pauseStartedAt) + { + pauseStartedAt = candidatePauseStartedAt; } + if (candidatePauseEndsAt > pauseEndsAt) + { + pauseEndsAt = candidatePauseEndsAt; + } + } - if (state.get().users.needsCleanup()) { state.mut().users.cleanup(); } - if (state.get().investors.needsCleanup()) { state.mut().investors.cleanup(); } - if (state.get().numberOfInvestedProjects.needsCleanup()) { state.mut().numberOfInvestedProjects.cleanup(); } - if (state.get().numberOfVotedProject.needsCleanup()) { state.mut().numberOfVotedProject.cleanup(); } - if (state.get().voteStatus.needsCleanup()) { state.mut().voteStatus.cleanup(); } + /** + * @brief Compares two Nostromo timestamps. + * @param a Left-hand date-time. + * @param b Right-hand date-time. + * @return `-1` if `a < b`, `0` if `a == b`, `1` if `a > b`. + */ + static sint32 dateCompare(const DateAndTime& a, const DateAndTime& b) + { + if (a < b) + { + return -1; + } + if (a > b) + { + return 1; + } + return 0; } - PRE_ACQUIRE_SHARES() - { - output.allowTransfer = true; - } + /** + * @brief Computes the difference in seconds between two `DateAndTime` values. + * @param a Start date-time. + * @param b End date-time. + * @param res Output difference in seconds, or `0` when `A >= B`. + */ + static void diffDateInSecond(const DateAndTime& a, const DateAndTime& b, uint64& res) + { + if (a >= b) + { + res = 0; + return; + } + res = div(a.durationMicrosec(b), NOST_MICROSECONDS_PER_SECOND); + } }; diff --git a/src/qpi/impl/qpi_system_impl.h b/src/qpi/impl/qpi_system_impl.h index 383dacad..bb98845d 100644 --- a/src/qpi/impl/qpi_system_impl.h +++ b/src/qpi/impl/qpi_system_impl.h @@ -1,14 +1,19 @@ -#pragma once - -#include "qpi/qpi.h" -#include "system.h" - -unsigned short QPI::QpiContextFunctionCall::epoch() const -{ - return system.epoch; -} - -unsigned int QPI::QpiContextFunctionCall::tick() const -{ - return system.tick; -} +#pragma once + +#include "qpi/qpi.h" +#include "system.h" + +unsigned short QPI::QpiContextFunctionCall::epoch() const +{ + return system.epoch; +} + +unsigned int QPI::QpiContextFunctionCall::tick() const +{ + return system.tick; +} + +unsigned int QPI::QpiContextFunctionCall::initialTick() const +{ + return system.initialTick; +} diff --git a/src/qpi/qpi_context.h b/src/qpi/qpi_context.h index 07854a4d..032e79af 100644 --- a/src/qpi/qpi_context.h +++ b/src/qpi/qpi_context.h @@ -185,6 +185,9 @@ namespace QPI inline uint32 tick( ) const; // [0..999'999'999] + inline uint32 initialTick( + ) const; + inline uint8 year( ) const; // [0..99] (0 = 2000, 1 = 2001, ..., 99 = 2099) diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index 6f9c8e67..a16d2233 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -1,1692 +1,4011 @@ #define NO_UEFI -#include -#include - #include "contract_testing.h" -static std::mt19937_64 rand64; +using namespace QPI; + +namespace +{ + static constexpr uint64 QX_ISSUE_ASSET_FEE = 1000000000ULL; + static constexpr uint64 QX_TRANSFER_ASSET_FEE = 1000000ULL; + static const id NOST_CONTRACT_ID(NOST_CONTRACT_INDEX, 0, 0, 0); +} // namespace + +class ContractTestingNOST : protected ContractTesting +{ +public: + ContractTestingNOST() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(NOST); + system.initialTick = system.tick; + system.epoch = contractDescriptions[NOST_CONTRACT_INDEX].constructionEpoch + 10; + callSystemProcedure(NOST_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QX); + callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); + setNow(2026, 1, 1, 9, 0, 0); + callSystemProcedure(NOST_CONTRACT_INDEX, END_TICK); + } + + void ensureUser(const id& user, sint64 amount = 1000) + { + if (getBalance(user) == 0) + { + increaseEnergy(user, amount); + } + } + + void seedUser(const id& user, sint64 amount = 2000000000LL) { increaseEnergy(user, amount); } + + void setNow(uint16 year, uint8 month, uint8 day, uint8 hour, uint8 minute, uint8 second) + { + utcTime.Year = year; + utcTime.Month = month; + utcTime.Day = day; + utcTime.Hour = hour; + utcTime.Minute = minute; + utcTime.Second = second; + utcTime.Nanosecond = 0; + updateQpiTime(); + } + + void advanceAndEndTick(uint64 milliseconds) + { + advanceTimeAndTick(milliseconds); + callSystemProcedure(NOST_CONTRACT_INDEX, END_TICK); + } + + void advanceTicks(uint32 count, uint64 millisecondsPerTick = 1000ULL) + { + for (uint32 i = 0; i < count; ++i) + { + advanceAndEndTick(millisecondsPerTick); + } + } + + void beginEpoch() + { + system.initialTick = system.tick; + ++system.epoch; + callSystemProcedure(NOST_CONTRACT_INDEX, BEGIN_EPOCH); + } + + void endEpoch() { callSystemProcedure(NOST_CONTRACT_INDEX, END_EPOCH); } + + sint64 issueAsset(const id& issuer, uint64 assetName, sint64 numberOfShares) + { + QX::IssueAsset_input input{}; + QX::IssueAsset_output output{}; + + input.assetName = assetName; + input.numberOfShares = numberOfShares; + input.unitOfMeasurement = 0; + input.numberOfDecimalPlaces = 0; + + seedUser(issuer, QX_ISSUE_ASSET_FEE); + invokeUserProcedure(QX_CONTRACT_INDEX, 1, input, output, issuer, QX_ISSUE_ASSET_FEE); + return output.issuedNumberOfShares; + } + + sint64 transferAsset(const id& owner, const id& recipient, const Asset& asset, sint64 numberOfShares) + { + QX::TransferShareOwnershipAndPossession_input input{}; + QX::TransferShareOwnershipAndPossession_output output{}; + + input.issuer = asset.issuer; + input.newOwnerAndPossessor = recipient; + input.assetName = asset.assetName; + input.numberOfShares = numberOfShares; + + seedUser(owner, QX_TRANSFER_ASSET_FEE); + invokeUserProcedure(QX_CONTRACT_INDEX, 2, input, output, owner, QX_TRANSFER_ASSET_FEE); + return output.transferredNumberOfShares; + } + + sint64 transferShareManagementRightsToNostromo(const id& owner, const Asset& asset, sint64 numberOfShares) + { + QX::TransferShareManagementRights_input input{}; + QX::TransferShareManagementRights_output output{}; + + input.asset = asset; + input.numberOfShares = numberOfShares; + input.newManagingContractIndex = NOST_CONTRACT_INDEX; + + invokeUserProcedure(QX_CONTRACT_INDEX, 9, input, output, owner, 0); + return output.transferredNumberOfShares; + } + + NOST::CreateAuction_output createAuction(const id& seller, const NOST::CreateAuction_input& input, + sint64 reward = NOST_PUBLIC_AUCTION_CREATION_FEE) + { + if (reward > 0) + { + seedUser(seller, reward); + } + else + { + ensureUser(seller); + } + return createAuctionWithFundedReward(seller, input, reward); + } + + NOST::CreateAuction_output createAuctionWithFundedReward(const id& seller, const NOST::CreateAuction_input& input, sint64 reward) + { + NOST::CreateAuction_output output{}; + invokeUserProcedure(NOST_CONTRACT_INDEX, 1, input, output, seller, reward); + return output; + } + + NOST::PlaceBid_output placeBid(const id& bidder, uint64 auctionIndex, uint64 quantity, uint64 bidAmount, sint64 reward) + { + NOST::PlaceBid_input input{}; + NOST::PlaceBid_output output{}; + + input.auctionIndex = auctionIndex; + input.quantity = quantity; + input.bidAmount = bidAmount; + + seedUser(bidder, reward); + invokeUserProcedure(NOST_CONTRACT_INDEX, 2, input, output, bidder, reward); + return output; + } + + NOST::PlaceBid_output placeBidWithFundedReward(const id& bidder, uint64 auctionIndex, uint64 quantity, uint64 bidAmount, sint64 reward) + { + NOST::PlaceBid_input input{}; + NOST::PlaceBid_output output{}; + + input.auctionIndex = auctionIndex; + input.quantity = quantity; + input.bidAmount = bidAmount; + + invokeUserProcedure(NOST_CONTRACT_INDEX, 2, input, output, bidder, reward); + return output; + } + + NOST::PlaceBid_output placeBatchBidWithRequiredReward(const id& bidder, uint64 auctionIndex, uint64 bidQuantity, uint64 bidAmount) + { + const NOST::CalculateBatchAuctionBidFee_output& calculation = calculateBatchAuctionBidFee(bidQuantity, bidAmount); + return placeBid(bidder, auctionIndex, bidQuantity, bidAmount, static_cast(calculation.requiredReward)); + } + + NOST::PlaceBid_output placeBatchBidWithFundedRequiredReward(const id& bidder, uint64 auctionIndex, uint64 bidQuantity, uint64 bidAmount) + { + const NOST::CalculateBatchAuctionBidFee_output& calculation = calculateBatchAuctionBidFee(bidQuantity, bidAmount); + return placeBidWithFundedReward(bidder, auctionIndex, bidQuantity, bidAmount, static_cast(calculation.requiredReward)); + } + + NOST::CancelAuction_output cancelAuction(const id& seller, uint64 auctionIndex, sint64 reward) + { + NOST::CancelAuction_input input{}; + NOST::CancelAuction_output output{}; + + input.auctionIndex = auctionIndex; + if (reward > 0) + { + seedUser(seller, reward); + } + else + { + ensureUser(seller); + } + invokeUserProcedure(NOST_CONTRACT_INDEX, 3, input, output, seller, reward); + return output; + } + + NOST::TransferShareManagementRights_output transferManagedSharesWithReward(const id& owner, const Asset& asset, sint64 numberOfShares, + uint32 contractIndex, sint64 reward) + { + if (reward > 0) + { + seedUser(owner, reward); + } + else + { + ensureUser(owner); + } + return transferManagedSharesWithFundedReward(owner, asset, numberOfShares, contractIndex, reward); + } + + NOST::TransferShareManagementRights_output transferManagedSharesWithFundedReward(const id& owner, const Asset& asset, sint64 numberOfShares, + uint32 contractIndex, sint64 reward) + { + NOST::TransferShareManagementRights_input input{}; + NOST::TransferShareManagementRights_output output{}; + + input.asset = asset; + input.numberOfShares = numberOfShares; + input.newManagingContractIndex = contractIndex; + + invokeUserProcedure(NOST_CONTRACT_INDEX, 4, input, output, owner, reward); + return output; + } + + NOST::TransferShareManagementRights_output transferManagedShares(const id& owner, const Asset& asset, sint64 numberOfShares, uint32 contractIndex) + { + syncCachedQxTransferFee(); + return transferManagedSharesWithReward(owner, asset, numberOfShares, contractIndex, getCachedQxTransferFee()); + } + + NOST::ResolvePendingStandardAuction_output resolvePendingStandardAuction(const id& seller, uint64 auctionIndex, bool acceptSale) + { + NOST::ResolvePendingStandardAuction_input input{}; + NOST::ResolvePendingStandardAuction_output output{}; + + input.auctionIndex = auctionIndex; + input.acceptSale = acceptSale ? 1 : 0; + + ensureUser(seller); + invokeUserProcedure(NOST_CONTRACT_INDEX, 5, input, output, seller, 0); + return output; + } + + NOST::SetAuctionFees_output setAuctionFees(const id& caller, const NOST::SetAuctionFees_input& input) + { + NOST::SetAuctionFees_output output{}; + ensureUser(caller); + invokeUserProcedure(NOST_CONTRACT_INDEX, 6, input, output, caller, 0); + return output; + } + + NOST::SetAuctionFeesByManagement_output setAuctionFeesByManagement(const id& caller, const NOST::SetAuctionFeesByManagement_input& input) + { + NOST::SetAuctionFeesByManagement_output output{}; + ensureUser(caller); + invokeUserProcedure(NOST_CONTRACT_INDEX, 7, input, output, caller, 0); + return output; + } + + NOST::SetManagement_output setManagement(const id& caller, const id& management) + { + NOST::SetManagement_input input{}; + NOST::SetManagement_output output{}; + + input.management = management; + ensureUser(caller); + invokeUserProcedure(NOST_CONTRACT_INDEX, 8, input, output, caller, 0); + return output; + } + + NOST::GetAuctionByIndex_output getAuction(uint64 auctionIndex) const + { + NOST::GetAuctionByIndex_input input{}; + NOST::GetAuctionByIndex_output output{}; + + input.auctionIndex = auctionIndex; + callFunction(NOST_CONTRACT_INDEX, 1, input, output); + return output; + } + + NOST::GetAuctionParticipant_output getParticipant(uint64 auctionIndex, const id& participant) const + { + NOST::GetAuctionParticipant_input input{}; + NOST::GetAuctionParticipant_output output{}; + + input.auctionIndex = auctionIndex; + input.participant = participant; + callFunction(NOST_CONTRACT_INDEX, 2, input, output); + return output; + } + + NOST::GetTicksBeforeAuctionLaunch_output getTicksBeforeAuctionLaunch() const + { + NOST::GetTicksBeforeAuctionLaunch_input input{}; + NOST::GetTicksBeforeAuctionLaunch_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 3, input, output); + return output; + } + + NOST::GetAuctionFees_output getAuctionFees() const + { + NOST::GetAuctionFees_input input{}; + NOST::GetAuctionFees_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 4, input, output); + return output; + } + + NOST::SetAuctionFees_input makeCoordinatorFeeInput(sint64 publicAuctionCreationFee) const + { + const NOST::GetAuctionFees_output& fees = getAuctionFees(); + NOST::SetAuctionFees_input input{}; + input.privateAuctionFee = fees.privateAuctionFee; + input.publicAuctionCreationFee = publicAuctionCreationFee; + input.auctionCancellationFeeBasisPoints = fees.auctionCancellationFeeBasisPoints; + input.managementFeeBasisPoints = fees.managementFeeBasisPoints; + input.developmentFeeBasisPoints = fees.developmentFeeBasisPoints; + input.takeoverCoordinatorFeeBasisPoints = fees.takeoverCoordinatorFeeBasisPoints; + input.shareholderDividendBasisPoints = fees.shareholderDividendBasisPoints; + input.shareholderFeeBasisPointsTier1 = fees.shareholderFeeBasisPointsTier1; + input.shareholderFeeBasisPointsTier2 = fees.shareholderFeeBasisPointsTier2; + input.shareholderFeeBasisPointsTier3 = fees.shareholderFeeBasisPointsTier3; + input.shareholderFeeBasisPointsTier4 = fees.shareholderFeeBasisPointsTier4; + return input; + } + + NOST::SetAuctionFeesByManagement_input makeManagementFeeInput(sint64 publicAuctionCreationFee) const + { + const NOST::GetAuctionFees_output& fees = getAuctionFees(); + NOST::SetAuctionFeesByManagement_input input{}; + input.privateAuctionFee = fees.privateAuctionFee; + input.publicAuctionCreationFee = publicAuctionCreationFee; + input.auctionCancellationFeeBasisPoints = fees.auctionCancellationFeeBasisPoints; + input.managementFeeBasisPoints = fees.managementFeeBasisPoints; + input.developmentFeeBasisPoints = fees.developmentFeeBasisPoints; + input.shareholderFeeBasisPointsTier1 = fees.shareholderFeeBasisPointsTier1; + input.shareholderFeeBasisPointsTier2 = fees.shareholderFeeBasisPointsTier2; + input.shareholderFeeBasisPointsTier3 = fees.shareholderFeeBasisPointsTier3; + input.shareholderFeeBasisPointsTier4 = fees.shareholderFeeBasisPointsTier4; + return input; + } + + NOST::CalculateBatchAuctionBidFee_output calculateBatchAuctionBidFee(uint64 bidQuantity, uint64 bidAmount) const + { + NOST::CalculateBatchAuctionBidFee_input input{}; + NOST::CalculateBatchAuctionBidFee_output output{}; + + input.bidQuantity = bidQuantity; + input.bidAmount = bidAmount; + callFunction(NOST_CONTRACT_INDEX, 20, input, output); + return output; + } + + NOST::GetFeeRecipients_output getFeeRecipients() const + { + NOST::GetFeeRecipients_input input{}; + NOST::GetFeeRecipients_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 5, input, output); + return output; + } + + NOST::GetClosedAuctionHistory_output getClosedAuctionHistory() const + { + NOST::GetClosedAuctionHistory_input input{}; + NOST::GetClosedAuctionHistory_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 6, input, output); + return output; + } + + NOST::GetRouteAllFeesToDevelopment_output getRouteAllFeesToDevelopmentPublic() const + { + NOST::GetRouteAllFeesToDevelopment_input input{}; + NOST::GetRouteAllFeesToDevelopment_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 7, input, output); + return output; + } + + NOST::GetContractStats_output getContractStats() const + { + NOST::GetContractStats_input input{}; + NOST::GetContractStats_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 8, input, output); + return output; + } + + NOST::GetAuctionSummaries_output getAuctionSummaries(uint64 offset, uint64 limit) const + { + NOST::GetAuctionSummaries_input input{}; + NOST::GetAuctionSummaries_output output{}; + + input.offset = offset; + input.limit = limit; + callFunction(NOST_CONTRACT_INDEX, 9, input, output); + return output; + } + + NOST::GetActiveAuctionIndices_output getActiveAuctionIndices(uint64 offset, uint64 limit) const + { + NOST::GetActiveAuctionIndices_input input{}; + NOST::GetActiveAuctionIndices_output output{}; + + input.offset = offset; + input.limit = limit; + callFunction(NOST_CONTRACT_INDEX, 10, input, output); + return output; + } + + NOST::GetAuctionsBySeller_output getAuctionsBySeller(const id& seller, uint64 offset, uint64 limit) const + { + NOST::GetAuctionsBySeller_input input{}; + NOST::GetAuctionsBySeller_output output{}; + + input.seller = seller; + input.offset = offset; + input.limit = limit; + callFunction(NOST_CONTRACT_INDEX, 11, input, output); + return output; + } + + NOST::GetAuctionByMetadataCid_output getAuctionByMetadataCid(const Array& metadataCid) const + { + NOST::GetAuctionByMetadataCid_input input{}; + NOST::GetAuctionByMetadataCid_output output{}; + + input.metadataIpfsCid = metadataCid; + callFunction(NOST_CONTRACT_INDEX, 12, input, output); + return output; + } + + NOST::GetAuctionSummariesByIndexBatch_output getAuctionSummariesByIndexBatch(const Array& auctionIndices, + uint64 count) const + { + NOST::GetAuctionSummariesByIndexBatch_input input{}; + NOST::GetAuctionSummariesByIndexBatch_output output{}; + + input.auctionIndices = auctionIndices; + input.count = count; + callFunction(NOST_CONTRACT_INDEX, 13, input, output); + return output; + } + + NOST::GetAuctionParticipants_output getAuctionParticipants(uint64 auctionIndex, uint64 offset, uint64 limit) const + { + NOST::GetAuctionParticipants_input input{}; + NOST::GetAuctionParticipants_output output{}; + + input.auctionIndex = auctionIndex; + input.offset = offset; + input.limit = limit; + callFunction(NOST_CONTRACT_INDEX, 14, input, output); + return output; + } + + NOST::GetUserParticipations_output getUserParticipations(const id& participant, uint64 offset, uint64 limit) const + { + NOST::GetUserParticipations_input input{}; + NOST::GetUserParticipations_output output{}; + + input.participant = participant; + input.offset = offset; + input.limit = limit; + callFunction(NOST_CONTRACT_INDEX, 15, input, output); + return output; + } + + NOST::GetLatestAuctionIndex_output getLatestAuctionIndex() const + { + NOST::GetLatestAuctionIndex_input input{}; + NOST::GetLatestAuctionIndex_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 16, input, output); + return output; + } + + NOST::GetAuctionCountBySeller_output getAuctionCountBySeller(const id& seller) const + { + NOST::GetAuctionCountBySeller_input input{}; + NOST::GetAuctionCountBySeller_output output{}; + + input.seller = seller; + callFunction(NOST_CONTRACT_INDEX, 17, input, output); + return output; + } + + NOST::GetAuctionAtCreationSnapshot_output getAuctionAtCreationSnapshot(uint64 auctionIndex) const + { + NOST::GetAuctionAtCreationSnapshot_input input{}; + NOST::GetAuctionAtCreationSnapshot_output output{}; + + input.auctionIndex = auctionIndex; + callFunction(NOST_CONTRACT_INDEX, 18, input, output); + return output; + } + + NOST::GetBatchAuctionBidAvailability_output getBatchAvailability(uint64 auctionIndex) const + { + NOST::GetBatchAuctionBidAvailability_input input{}; + NOST::GetBatchAuctionBidAvailability_output output{}; + + input.auctionIndex = auctionIndex; + callFunction(NOST_CONTRACT_INDEX, 19, input, output); + return output; + } + + NOST::GetPendingServiceFeePool_output getPendingServiceFeePool() const + { + NOST::GetPendingServiceFeePool_input input{}; + NOST::GetPendingServiceFeePool_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 21, input, output); + return output; + } + + NOST::GetFeeReserveGuardState_output getFeeReserveGuardState() const + { + NOST::GetFeeReserveGuardState_input input{}; + NOST::GetFeeReserveGuardState_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 22, input, output); + return output; + } + + NOST::GetPendingPayout_output getPendingPayout(const id& account) const + { + NOST::GetPendingPayout_input input{}; + NOST::GetPendingPayout_output output{}; + input.account = account; + callFunction(NOST_CONTRACT_INDEX, 23, input, output); + return output; + } + + NOST::GetNostromoFeePool_output getNostromoFeePool() const + { + NOST::GetNostromoFeePool_input input{}; + NOST::GetNostromoFeePool_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 24, input, output); + return output; + } + + NOST::SetFeeReserveGuardConfig_output setFeeReserveGuardConfig(const id& caller, uint64 dropBasisPoints, uint64 windowSeconds) + { + NOST::SetFeeReserveGuardConfig_input input{}; + NOST::SetFeeReserveGuardConfig_output output{}; + + input.dropBasisPoints = dropBasisPoints; + input.windowSeconds = windowSeconds; + ensureUser(caller); + invokeUserProcedure(NOST_CONTRACT_INDEX, 9, input, output, caller, 0); + return output; + } + + NOST::SetEmergencyPause_output setEmergencyPause(const id& caller, bool paused) + { + NOST::SetEmergencyPause_input input{}; + NOST::SetEmergencyPause_output output{}; + + input.paused = paused ? 1 : 0; + ensureUser(caller); + invokeUserProcedure(NOST_CONTRACT_INDEX, 10, input, output, caller, 0); + return output; + } + + NOST::StateData& stateData() { return *reinterpret_cast(contractStates[NOST_CONTRACT_INDEX]); } + const NOST::StateData& stateData() const { return *reinterpret_cast(contractStates[NOST_CONTRACT_INDEX]); } + QX::StateData& qxStateData() { return *reinterpret_cast(contractStates[QX_CONTRACT_INDEX]); } + + void setRouteAllFeesToDevelopment(uint8 enabled) { stateData().routeAllFeesToDevelopment = enabled; } + uint8 getRouteAllFeesToDevelopment() const { return stateData().routeAllFeesToDevelopment; } + void syncCachedQxTransferFee() { stateData().qxTransferFee = qxStateData()._transferFee; } + uint32 getCachedQxTransferFee() const { return stateData().qxTransferFee; } + + sint64 managedShares(const Asset& asset, const id& owner) const + { + return numberOfPossessedShares(asset.assetName, asset.issuer, owner, owner, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX); + } + + sint64 sharesManagedBy(const Asset& asset, const id& owner, uint32 contractIndex) const + { + return numberOfPossessedShares(asset.assetName, asset.issuer, owner, owner, contractIndex, contractIndex); + } + + sint64 plainShares(const Asset& asset, const id& owner) const + { + return numberOfShares(asset, AssetOwnershipSelect::byOwner(owner), AssetPossessionSelect::byPossessor(owner)); + } + + static Array makeMetadataCid() + { + Array cid{}; + const char* cidText = "bafybeigdyrzt2a3x4m5n6p7qrstuvwx234567abcdefghijklmnopqrst"; + for (uint64 i = 0; cidText[i] != 0 && i < NOST_AUCTION_METADATA_CID_LENGTH; ++i) + { + cid.set(i, static_cast(cidText[i])); + } + return cid; + } + + static Array makeInvalidMetadataCidFirstChar() + { + auto cid = makeMetadataCid(); + cid.set(0, 'c'); + return cid; + } + + static Array makeInvalidMetadataCidUppercase() + { + auto cid = makeMetadataCid(); + cid.set(5, 'A'); + return cid; + } + + static Array makeSingleLot(const Asset& asset, sint64 quantity) + { + Array lot{}; + NOST::AuctionAssetEntry entry{}; + + entry.asset = asset; + entry.quantity = quantity; + lot.set(0, entry); + return lot; + } + + static Array makeLot(std::initializer_list entries) + { + Array lot{}; + uint64 index = 0; + for (const auto& entry : entries) + { + lot.set(index++, entry); + } + return lot; + } + + static Array makeAllowedWallets(std::initializer_list wallets) + { + Array allowed{}; + uint64 index = 0; + for (const auto& wallet : wallets) + { + allowed.set(index++, wallet); + } + return allowed; + } + + static Array + makeRequiredAccessAssets(std::initializer_list assets) + { + Array required{}; + uint64 index = 0; + for (const auto& asset : assets) + { + required.set(index++, asset); + } + return required; + } + + static NOST::CreateAuction_input makeBatchAuctionInput(const Asset& asset, sint64 quantity, uint64 salePrice = 10) + { + NOST::CreateAuction_input input{}; + input.metadataIpfsCid = makeMetadataCid(); + input.auctionLotItems = makeSingleLot(asset, quantity); + input.minimumPurchaseQuantity = 1; + input.salePrice = salePrice; + input.durationDays = 1; + input.auctionType = static_cast(NOST::EAuctionType::Batch); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Public); + return input; + } + + static NOST::CreateAuction_input makeStandardAuctionInput(const Array& lot, + uint64 initialPrice = NOST_STANDARD_MIN_PRICE, + uint64 salePrice = NOST_STANDARD_MIN_PRICE, + uint64 minimumBidIncrement = NOST_STANDARD_MIN_BID_INCREMENT, uint64 buyNowPrice = 0) + { + NOST::CreateAuction_input input{}; + input.metadataIpfsCid = makeMetadataCid(); + input.auctionLotItems = lot; + input.minimumPurchaseQuantity = 1; + input.initialPrice = initialPrice; + input.salePrice = salePrice; + input.minimumBidIncrement = minimumBidIncrement; + input.buyNowPrice = buyNowPrice; + input.durationDays = 1; + input.auctionType = static_cast(NOST::EAuctionType::Standard); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Public); + return input; + } + + sint64 expectedDividendPoolIncrease(uint64 addedDividendAmount) const + { + const uint64 poolBefore = stateData().auctionShareholderDividendPool; + const uint64 poolAfterFunding = poolBefore + addedDividendAmount; + return static_cast(poolAfterFunding % NUMBER_OF_COMPUTORS) - static_cast(poolBefore); + } + + static id managementWallet() + { + return ID(_I, _G, _P, _Z, _X, _Q, _O, _R, _J, _Y, _Q, _P, _A, _G, _V, _A, _B, _N, _T, _N, _I, _S, _O, _Y, _T, _M, _T, _A, _N, _M, _K, _Z, _A, + _S, _T, _P, _P, _G, _Z, _O, _N, _A, _Q, _J, _X, _Q, _O, _S, _W, _Q, _O, _V, _J, _C, _K, _D); + } + + static id developmentWallet() + { + return ID(_D, _Q, _V, _H, _M, _Z, _F, _C, _W, _O, _K, _M, _H, _F, _B, _H, _L, _X, _U, _I, _U, _G, _P, _P, _X, _R, _Z, _C, _U, _V, _S, _N, _J, + _F, _Z, _J, _F, _M, _Q, _M, _Y, _D, _B, _X, _E, _S, _E, _A, _T, _M, _W, _L, _K, _N, _L, _D); + } + + static id takeoverCoordinatorWallet() + { + return ID(_X, _J, _O, _S, _N, _L, _T, _Z, _V, _V, _H, _N, _Z, _C, _B, _Y, _X, _I, _E, _V, _N, _E, _P, _P, _B, _O, _Q, _A, _W, _D, _B, _V, _G, + _E, _N, _Z, _O, _X, _S, _V, _O, _B, _K, _G, _Z, _C, _C, _F, _D, _B, _D, _M, _T, _M, _L, _C); + } +}; -static unsigned long long random(unsigned long long minValue, unsigned long long maxValue) +static bool containsWallet(const Array& wallets, uint64 count, const id& wallet) { - if(minValue > maxValue) - { - return 0; - } - return minValue + rand64() % (maxValue - minValue); + for (uint64 index = 0; index < count; ++index) + { + if (wallets.get(index) == wallet) + { + return true; + } + } + return false; } -static id getUser(unsigned long long i) +static bool containsAccessAsset(const Array& assets, uint64 count, + const NOST::AuctionAssetEntry& expected) { - return id(i, i / 2 + 4, i + 10, i * 3 + 8); + for (uint64 index = 0; index < count; ++index) + { + if (assets.get(index).asset == expected.asset && assets.get(index).quantity == expected.quantity) + { + return true; + } + } + return false; } -static std::vector getRandomUsers(unsigned int totalUsers, unsigned int maxNum) +static bool containsAuctionIndex(const Array& auctionIndices, uint64 count, uint64 auctionIndex) { - unsigned long long userCount = random(0, maxNum); - std::vector users; - users.reserve(userCount); - for (unsigned int i = 0; i < userCount; ++i) - { - unsigned long long userIdx = random(0, totalUsers - 1); - users.push_back(getUser(userIdx)); - } - return users; + const uint64 boundedCount = count < auctionIndices.capacity() ? count : auctionIndices.capacity(); + for (uint64 index = 0; index < boundedCount; ++index) + { + if (auctionIndices.get(index) == auctionIndex) + { + return true; + } + } + return false; } -class NostromoChecker : public NOST, public NOST::StateData +static void expectAuctionFeesEqual(const NOST::GetAuctionFees_output& actual, const NOST::GetAuctionFees_output& expected) { -public: - void registerChecker(id registerId, uint32 tierLevel, uint32 indexOfRegister) - { - EXPECT_EQ(users.contains(registerId), 1); - uint8 stateTierLevel; - users.get(registerId, stateTierLevel); - EXPECT_EQ(tierLevel, stateTierLevel); - } - void countOfRegisterChecker(uint32 totalUser) - { - EXPECT_EQ(totalUser, numberOfRegister); - } - void logoutFromTierChecker(id registerId) - { - EXPECT_EQ(users.contains(registerId), 0); - } - void numberOfCreatedProjectChecker(uint32 numberOfProjects) - { - EXPECT_EQ(numberOfProjects, numberOfCreatedProject); - } - void createdProjectChecker(uint32 indexOfProject, id creator, uint64 assetName, uint32 supply, uint32 startYear, uint32 startMonth, uint32 startDay, uint32 startHour, uint32 endYear, uint32 endMonth, uint32 endDay, uint32 endHour) - { - uint32 startDate, endDate; - NOST::packNostromoDate(startYear, startMonth, startDay, startHour, 0, 0, startDate); - NOST::packNostromoDate(endYear, endMonth, endDay, endHour, 0, 0, endDate); - - EXPECT_EQ(tokens.contains(assetName), 1); - EXPECT_EQ(projects.get(indexOfProject).creator, creator); - EXPECT_EQ(projects.get(indexOfProject).isCreatedFundarasing, 0); - EXPECT_EQ(projects.get(indexOfProject).numberOfNo, 0); - EXPECT_EQ(projects.get(indexOfProject).numberOfYes, 0); - EXPECT_EQ(projects.get(indexOfProject).supplyOfToken, supply); - EXPECT_EQ(projects.get(indexOfProject).tokenName, assetName); - EXPECT_EQ(projects.get(indexOfProject).startDate, startDate); - EXPECT_EQ(projects.get(indexOfProject).endDate, endDate); - } - void epochRevenueChecker(uint64 amountOfRevenue) - { - EXPECT_EQ(amountOfRevenue, epochRevenue); - } - void totalPoolWeightChecker(uint32 totalWeight) - { - EXPECT_EQ(totalWeight, totalPoolWeight); - } - void voteInProjectChecker(uint32 indexOfProject, uint32 numberOfYes, uint32 numberOfNo) - { - EXPECT_EQ(projects.get(indexOfProject).numberOfYes, numberOfYes); - EXPECT_EQ(projects.get(indexOfProject).numberOfNo, numberOfNo); - } - void numberOfVotedProjectAndVotedListChecker(id registerId, uint32 numberOfProject, Array votedList) - { - uint32 count; - numberOfVotedProject.get(registerId, count); - EXPECT_EQ(count, numberOfProject); - - Array vote; - voteStatus.get(registerId, vote); - for (uint32 i = 0; i < count; i++) - { - EXPECT_EQ(vote.get(i), votedList.get(i)); - } - } - void countOfFundraisingChecker(uint32 count) - { - EXPECT_EQ(count, numberOfFundraising); - } - void createFundraisingChecker(const id& registerId, - uint64 tokenPrice, - uint64 soldAmount, - uint64 requiredFunds, - - uint32 indexOfProject, - uint32 firstPhaseStartYear, - uint32 firstPhaseStartMonth, - uint32 firstPhaseStartDay, - uint32 firstPhaseStartHour, - uint32 firstPhaseEndYear, - uint32 firstPhaseEndMonth, - uint32 firstPhaseEndDay, - uint32 firstPhaseEndHour, - - uint32 secondPhaseStartYear, - uint32 secondPhaseStartMonth, - uint32 secondPhaseStartDay, - uint32 secondPhaseStartHour, - uint32 secondPhaseEndYear, - uint32 secondPhaseEndMonth, - uint32 secondPhaseEndDay, - uint32 secondPhaseEndHour, - - uint32 thirdPhaseStartYear, - uint32 thirdPhaseStartMonth, - uint32 thirdPhaseStartDay, - uint32 thirdPhaseStartHour, - uint32 thirdPhaseEndYear, - uint32 thirdPhaseEndMonth, - uint32 thirdPhaseEndDay, - uint32 thirdPhaseEndHour, - - uint32 listingStartYear, - uint32 listingStartMonth, - uint32 listingStartDay, - uint32 listingStartHour, - - uint32 cliffEndYear, - uint32 cliffEndMonth, - uint32 cliffEndDay, - uint32 cliffEndHour, - - uint32 vestingEndYear, - uint32 vestingEndMonth, - uint32 vestingEndDay, - uint32 vestingEndHour, - - uint8 threshold, - uint8 TGE, - uint8 stepOfVesting, - - uint32 indexOfFundraising) - { - uint32 firstPhaseStartDate_t, secondPhaseStartDate_t, thirdPhaseStartDate_t, firstPhaseEndDate_t, secondPhaseEndDate_t, thirdPhaseEndDate_t, listingStartDate_t, cliffEndDate_t, vestingEndDate_t; - NOST::packNostromoDate(firstPhaseStartYear, firstPhaseStartMonth, firstPhaseStartDay, firstPhaseStartHour, 0, 0, firstPhaseStartDate_t); - NOST::packNostromoDate(secondPhaseStartYear, secondPhaseStartMonth, secondPhaseStartDay, secondPhaseStartHour, 0, 0, secondPhaseStartDate_t); - NOST::packNostromoDate(thirdPhaseStartYear, thirdPhaseStartMonth, thirdPhaseStartDay, thirdPhaseStartHour, 0, 0, thirdPhaseStartDate_t); - NOST::packNostromoDate(firstPhaseEndYear, firstPhaseEndMonth, firstPhaseEndDay, firstPhaseEndHour, 0, 0, firstPhaseEndDate_t); - NOST::packNostromoDate(secondPhaseEndYear, secondPhaseEndMonth, secondPhaseEndDay, secondPhaseEndHour, 0, 0, secondPhaseEndDate_t); - NOST::packNostromoDate(thirdPhaseEndYear, thirdPhaseEndMonth, thirdPhaseEndDay, thirdPhaseEndHour, 0, 0, thirdPhaseEndDate_t); - NOST::packNostromoDate(listingStartYear, listingStartMonth, listingStartDay, listingStartHour, 0, 0, listingStartDate_t); - NOST::packNostromoDate(cliffEndYear, cliffEndMonth, cliffEndDay, cliffEndHour, 0, 0, cliffEndDate_t); - NOST::packNostromoDate(vestingEndYear, vestingEndMonth, vestingEndDay, vestingEndHour, 0, 0, vestingEndDate_t); - - EXPECT_EQ(registerId, projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator); - EXPECT_EQ(tokenPrice, fundaraisings.get(indexOfFundraising).tokenPrice); - - EXPECT_EQ(soldAmount, fundaraisings.get(indexOfFundraising).soldAmount); - EXPECT_EQ(requiredFunds, fundaraisings.get(indexOfFundraising).requiredFunds); - EXPECT_EQ(indexOfProject, fundaraisings.get(indexOfFundraising).indexOfProject); - EXPECT_EQ(firstPhaseStartDate_t, fundaraisings.get(indexOfFundraising).firstPhaseStartDate); - EXPECT_EQ(secondPhaseStartDate_t, fundaraisings.get(indexOfFundraising).secondPhaseStartDate); - EXPECT_EQ(thirdPhaseStartDate_t, fundaraisings.get(indexOfFundraising).thirdPhaseStartDate); - EXPECT_EQ(firstPhaseEndDate_t, fundaraisings.get(indexOfFundraising).firstPhaseEndDate); - EXPECT_EQ(secondPhaseEndDate_t, fundaraisings.get(indexOfFundraising).secondPhaseEndDate); - EXPECT_EQ(thirdPhaseEndDate_t, fundaraisings.get(indexOfFundraising).thirdPhaseEndDate); - EXPECT_EQ(listingStartDate_t, fundaraisings.get(indexOfFundraising).listingStartDate); - EXPECT_EQ(cliffEndDate_t, fundaraisings.get(indexOfFundraising).cliffEndDate); - EXPECT_EQ(vestingEndDate_t, fundaraisings.get(indexOfFundraising).vestingEndDate); - EXPECT_EQ(threshold, fundaraisings.get(indexOfFundraising).threshold); - EXPECT_EQ(TGE, fundaraisings.get(indexOfFundraising).TGE); - EXPECT_EQ(stepOfVesting, fundaraisings.get(indexOfFundraising).stepOfVesting); - - } - uint8 getTierLevel(id registerId) - { - if (users.contains(registerId)) - { - uint8 tierLevel; - users.get(registerId, tierLevel); - return tierLevel; - } - return 0; - } - uint64 getInvestedAmount(uint32 indexOfFundraising, id registerId) - { - investors.get(registerId, tmpInvestedList); - uint32 numberOfProject; - numberOfInvestedProjects.get(registerId, numberOfProject); - - for (uint32 i = 0; i < numberOfProject; i++) + EXPECT_EQ(actual.privateAuctionFee, expected.privateAuctionFee); + EXPECT_EQ(actual.publicAuctionCreationFee, expected.publicAuctionCreationFee); + EXPECT_EQ(actual.auctionCancellationFeeBasisPoints, expected.auctionCancellationFeeBasisPoints); + EXPECT_EQ(actual.managementFeeBasisPoints, expected.managementFeeBasisPoints); + EXPECT_EQ(actual.developmentFeeBasisPoints, expected.developmentFeeBasisPoints); + EXPECT_EQ(actual.takeoverCoordinatorFeeBasisPoints, expected.takeoverCoordinatorFeeBasisPoints); + EXPECT_EQ(actual.shareholderDividendBasisPoints, expected.shareholderDividendBasisPoints); + EXPECT_EQ(actual.shareholderFeeBasisPointsTier1, expected.shareholderFeeBasisPointsTier1); + EXPECT_EQ(actual.shareholderFeeBasisPointsTier2, expected.shareholderFeeBasisPointsTier2); + EXPECT_EQ(actual.shareholderFeeBasisPointsTier3, expected.shareholderFeeBasisPointsTier3); + EXPECT_EQ(actual.shareholderFeeBasisPointsTier4, expected.shareholderFeeBasisPointsTier4); +} + +TEST(ContractNostromoAuction, InitialStateAndGettersAuction) +{ + ContractTestingNOST nostromo; + + const auto fees = nostromo.getAuctionFees(); + EXPECT_EQ(fees.privateAuctionFee, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + EXPECT_EQ(fees.publicAuctionCreationFee, NOST_PUBLIC_AUCTION_CREATION_FEE); + EXPECT_EQ(fees.auctionCancellationFeeBasisPoints, NOST_DEFAULT_AUCTION_CANCELLATION_FEE_BP); + EXPECT_EQ(fees.managementFeeBasisPoints, NOST_DEFAULT_AUCTION_MANAGEMENT_FEE_BP); + EXPECT_EQ(fees.developmentFeeBasisPoints, NOST_DEFAULT_AUCTION_DEVELOPMENT_FEE_BP); + EXPECT_EQ(fees.takeoverCoordinatorFeeBasisPoints, NOST_DEFAULT_AUCTION_TAKEOVER_COORDINATOR_FEE_BP); + EXPECT_EQ(fees.shareholderDividendBasisPoints, NOST_DEFAULT_AUCTION_SHAREHOLDER_DIVIDEND_BP); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier1, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_1); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier2, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_2); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier3, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_3); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier4, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4); + + const auto recipients = nostromo.getFeeRecipients(); + EXPECT_EQ(recipients.management, ContractTestingNOST::managementWallet()); + EXPECT_EQ(recipients.development, ContractTestingNOST::developmentWallet()); + EXPECT_EQ(recipients.takeoverCoordinator, ContractTestingNOST::takeoverCoordinatorWallet()); + + const uint64 missingAuction = 777; + const id missingParticipant(888, 0, 0, 0); + const auto auctionOutput = nostromo.getAuction(missingAuction); + const auto participantOutput = nostromo.getParticipant(missingAuction, missingParticipant); + const auto launchPause = nostromo.getTicksBeforeAuctionLaunch(); + + EXPECT_EQ(auctionOutput.auction.core.auctionIndex, 0ULL); + EXPECT_EQ(participantOutput.found, 0); + EXPECT_EQ(launchPause.ticks, 0U); + EXPECT_EQ(nostromo.getClosedAuctionHistory().totalEntries, 0ULL); + EXPECT_EQ(nostromo.getRouteAllFeesToDevelopmentPublic().enabled, NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT); + + nostromo.setRouteAllFeesToDevelopment(1); + EXPECT_EQ(nostromo.getRouteAllFeesToDevelopmentPublic().enabled, 1); + nostromo.setRouteAllFeesToDevelopment(0); + EXPECT_EQ(nostromo.getRouteAllFeesToDevelopmentPublic().enabled, 0); + + nostromo.beginEpoch(); + EXPECT_EQ(nostromo.getCachedQxTransferFee(), nostromo.qxStateData()._transferFee); + EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, 0U); +} + +TEST(ContractNostromoAuction, AuctionIndexAndExpandedGetterSurfaceAuction) +{ + ContractTestingNOST nostromo; + const id sellerA(31, 32, 33, 34); + const id sellerB(35, 36, 37, 38); + const id bidderA(39, 40, 41, 42); + const id bidderB(43, 44, 45, 46); + const uint64 assetNameA = assetNameFromString("IDXGTA"); + const uint64 assetNameB = assetNameFromString("IDXGTB"); + const uint64 assetNameC = assetNameFromString("IDXGTC"); + const Asset assetA{sellerA, assetNameA}; + const Asset assetB{sellerA, assetNameB}; + const Asset assetC{sellerB, assetNameC}; + + EXPECT_EQ(nostromo.getLatestAuctionIndex().found, 0); + EXPECT_EQ(nostromo.getLatestAuctionIndex().auctionIndex, 0ULL); + + EXPECT_EQ(nostromo.issueAsset(sellerA, assetNameA, 3), 3); + EXPECT_EQ(nostromo.issueAsset(sellerA, assetNameB, 2), 2); + EXPECT_EQ(nostromo.issueAsset(sellerB, assetNameC, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(sellerA, assetA, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(sellerA, assetB, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(sellerB, assetC, 1), 1); + + auto inputA = ContractTestingNOST::makeBatchAuctionInput(assetA, 3, 10); + auto inputB = ContractTestingNOST::makeBatchAuctionInput(assetB, 2, 12); + auto inputC = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetC, 1)); + inputB.metadataIpfsCid.set(10, '2'); + inputC.metadataIpfsCid.set(10, '3'); + + const auto createA = nostromo.createAuction(sellerA, inputA); + const auto createB = nostromo.createAuction(sellerA, inputB); + const auto createC = nostromo.createAuction(sellerB, inputC); + ASSERT_EQ(createA.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(createB.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(createC.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(createA.auctionIndex, 0ULL); + EXPECT_EQ(createB.auctionIndex, 1ULL); + EXPECT_EQ(createC.auctionIndex, 2ULL); + + EXPECT_EQ(nostromo.getLatestAuctionIndex().found, 1); + EXPECT_EQ(nostromo.getLatestAuctionIndex().auctionIndex, 2ULL); + EXPECT_EQ(nostromo.getAuction(createB.auctionIndex).auction.core.auctionIndex, 1ULL); + EXPECT_EQ(nostromo.getAuctionAtCreationSnapshot(createC.auctionIndex).seller, sellerB); + EXPECT_EQ(nostromo.getAuctionAtCreationSnapshot(createC.auctionIndex).auctionIndex, 2ULL); + + const auto summaries = nostromo.getAuctionSummaries(0, 64); + EXPECT_EQ(summaries.totalCount, 3ULL); + EXPECT_EQ(summaries.returnedCount, 3ULL); + EXPECT_EQ(summaries.auctions.get(0).auctionIndex, 0ULL); + EXPECT_EQ(summaries.auctions.get(2).seller, sellerB); + + const auto sellerAList = nostromo.getAuctionsBySeller(sellerA, 0, 64); + EXPECT_EQ(sellerAList.totalCount, 2ULL); + EXPECT_EQ(sellerAList.returnedCount, 2ULL); + EXPECT_EQ(nostromo.getAuctionCountBySeller(sellerA).count, 2ULL); + EXPECT_EQ(nostromo.getAuctionCountBySeller(sellerB).count, 1ULL); + + const auto metadataLookup = nostromo.getAuctionByMetadataCid(inputB.metadataIpfsCid); + EXPECT_EQ(metadataLookup.found, 1); + EXPECT_EQ(metadataLookup.auctionIndex, 1ULL); + EXPECT_EQ(metadataLookup.auction.seller, sellerA); + + Array requestedIndices{}; + requestedIndices.set(0, createC.auctionIndex); + requestedIndices.set(1, 999); + requestedIndices.set(2, createA.auctionIndex); + const auto batch = nostromo.getAuctionSummariesByIndexBatch(requestedIndices, 3); + EXPECT_EQ(batch.returnedCount, 2ULL); + EXPECT_EQ(batch.found.get(0), 1); + EXPECT_EQ(batch.found.get(1), 0); + EXPECT_EQ(batch.found.get(2), 1); + EXPECT_EQ(batch.auctions.get(0).auctionIndex, 2ULL); + EXPECT_EQ(batch.auctions.get(2).auctionIndex, 0ULL); + + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createA.auctionIndex, 2, 11).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderB, createA.auctionIndex, 1, 15).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidderA, createC.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, + NOST::EAuctionError::Success); + + const auto active = nostromo.getActiveAuctionIndices(0, 64); + EXPECT_EQ(active.totalCount, 3ULL); + EXPECT_EQ(active.returnedCount, 3ULL); + EXPECT_EQ(active.auctionIndices.get(1), 1ULL); + + const auto participants = nostromo.getAuctionParticipants(createA.auctionIndex, 0, 64); + EXPECT_EQ(participants.totalCount, 2ULL); + EXPECT_EQ(participants.returnedCount, 2ULL); + EXPECT_TRUE(participants.participants.get(0).participant == bidderA || participants.participants.get(1).participant == bidderA); + + const auto bidderAParticipations = nostromo.getUserParticipations(bidderA, 0, 64); + EXPECT_EQ(bidderAParticipations.totalCount, 2ULL); + EXPECT_EQ(bidderAParticipations.returnedCount, 2ULL); + + const auto stats = nostromo.getContractStats(); + EXPECT_EQ(stats.stats.totalAuctionsCreated, 3ULL); + EXPECT_EQ(stats.stats.activeAuctionCount, 3ULL); + EXPECT_EQ(stats.stats.participantCount, 3ULL); +} + +TEST(ContractNostromoAuction, RetainedAuctionGetterPaginationAcrossLiveAndClosedStorageAuction) +{ + ContractTestingNOST nostromo; + const id sellerA(61, 62, 63, 64); + const id sellerB(65, 66, 67, 68); + const id missingSeller(69, 70, 71, 72); + const auto makeAuction = + [](uint64 auctionIndex, const id& seller, NOST::EAuctionStatus status, const Array& metadataCid) + { + NOST::AuctionData auction{}; + auction.core.auctionIndex = auctionIndex; + auction.core.seller = seller; + auction.core.status = status; + auction.core.metadataIpfsCid = metadataCid; + return auction; + }; + auto sharedCid = ContractTestingNOST::makeMetadataCid(); + auto cidAtIndex1 = ContractTestingNOST::makeMetadataCid(); + auto cidAtIndex5 = ContractTestingNOST::makeMetadataCid(); + auto cidAtIndex6 = ContractTestingNOST::makeMetadataCid(); + auto cidAtIndex7 = ContractTestingNOST::makeMetadataCid(); + auto missingCid = ContractTestingNOST::makeMetadataCid(); + sharedCid.set(10, 's'); + cidAtIndex1.set(10, 'a'); + cidAtIndex5.set(10, 'c'); + cidAtIndex6.set(10, 'd'); + cidAtIndex7.set(10, 'e'); + missingCid.set(10, 'm'); + + // Insert live auctions out of creation order to ensure pagination does not depend on physical hash-map order. + ASSERT_NE(nostromo.stateData().auctionList.set(7, makeAuction(7, sellerA, NOST::EAuctionStatus::Active, cidAtIndex7)), NULL_INDEX); + ASSERT_NE(nostromo.stateData().auctionList.set(1, makeAuction(1, sellerB, NOST::EAuctionStatus::Active, cidAtIndex1)), NULL_INDEX); + ASSERT_NE(nostromo.stateData().auctionList.set(9, makeAuction(9, sellerA, NOST::EAuctionStatus::Active, sharedCid)), NULL_INDEX); + ASSERT_NE(nostromo.stateData().auctionList.set(5, makeAuction(5, sellerA, NOST::EAuctionStatus::PendingSellerDecision, cidAtIndex5)), NULL_INDEX); + + // A partially filled history verifies that uninitialized ring capacity is not scanned as retained data. + nostromo.stateData().closedAuctionHistory.set(0, makeAuction(2, sellerA, NOST::EAuctionStatus::Finalized, sharedCid)); + nostromo.stateData().closedAuctionHistory.set(1, makeAuction(6, sellerB, NOST::EAuctionStatus::Cancelled, cidAtIndex6)); + nostromo.stateData().closedAuctionHistoryCounter = 2; + + const auto summaries = nostromo.getAuctionSummaries(1, 3); + ASSERT_EQ(summaries.totalCount, 6ULL); + ASSERT_EQ(summaries.returnedCount, 3ULL); + EXPECT_EQ(summaries.auctions.get(0).auctionIndex, 2ULL); + EXPECT_EQ(summaries.auctions.get(1).auctionIndex, 5ULL); + EXPECT_EQ(summaries.auctions.get(2).auctionIndex, 6ULL); + + const auto active = nostromo.getActiveAuctionIndices(1, 2); + ASSERT_EQ(active.totalCount, 4ULL); + ASSERT_EQ(active.returnedCount, 2ULL); + EXPECT_EQ(active.auctionIndices.get(0), 5ULL); + EXPECT_EQ(active.auctionIndices.get(1), 7ULL); + EXPECT_EQ(nostromo.getActiveAuctionIndices(0, 0).returnedCount, 0ULL); + EXPECT_EQ(nostromo.getActiveAuctionIndices(active.totalCount, 2).returnedCount, 0ULL); + + const auto sellerPage = nostromo.getAuctionsBySeller(sellerA, 1, 2); + ASSERT_EQ(sellerPage.totalCount, 4ULL); + ASSERT_EQ(sellerPage.returnedCount, 2ULL); + EXPECT_EQ(sellerPage.auctions.get(0).auctionIndex, 5ULL); + EXPECT_EQ(sellerPage.auctions.get(1).auctionIndex, 7ULL); + EXPECT_EQ(nostromo.getAuctionCountBySeller(sellerA).count, sellerPage.totalCount); + EXPECT_EQ(nostromo.getAuctionCountBySeller(sellerB).count, 2ULL); + EXPECT_EQ(nostromo.getAuctionCountBySeller(missingSeller).count, 0ULL); + EXPECT_EQ(nostromo.getAuctionsBySeller(missingSeller, 0, 2).returnedCount, 0ULL); + + const auto sharedCidLookup = nostromo.getAuctionByMetadataCid(sharedCid); + ASSERT_EQ(sharedCidLookup.found, 1); + EXPECT_EQ(sharedCidLookup.auctionIndex, 2ULL); + EXPECT_EQ(sharedCidLookup.auction.seller, sellerA); + EXPECT_EQ(nostromo.getAuctionByMetadataCid(missingCid).found, 0); +} + +TEST(ContractNostromoAuction, TransferShareManagementRightsAuction) +{ + ContractTestingNOST nostromo; + const id owner(1, 2, 3, 4); + const uint64 assetName = assetNameFromString("NOSTTR"); + const Asset asset{owner, assetName}; + + EXPECT_EQ(nostromo.issueAsset(owner, assetName, 10), 10); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(owner, asset, 7), 7); + EXPECT_EQ(nostromo.managedShares(asset, owner), 7); + EXPECT_EQ(nostromo.sharesManagedBy(asset, owner, QX_CONTRACT_INDEX), 3); + + const auto invalidZeroShares = nostromo.transferManagedShares(owner, asset, 0, QX_CONTRACT_INDEX); + EXPECT_EQ(invalidZeroShares.transferredNumberOfShares, 0); + EXPECT_EQ(invalidZeroShares.errorCode, NOST::EAuctionError::InvalidInput); + + Asset zeroAsset{}; + const auto invalidZeroAsset = nostromo.transferManagedShares(owner, zeroAsset, 1, QX_CONTRACT_INDEX); + EXPECT_EQ(invalidZeroAsset.transferredNumberOfShares, 0); + EXPECT_EQ(invalidZeroAsset.errorCode, NOST::EAuctionError::InvalidInput); + + const auto invalidZeroContract = nostromo.transferManagedShares(owner, asset, 1, 0); + EXPECT_EQ(invalidZeroContract.transferredNumberOfShares, 0); + EXPECT_EQ(invalidZeroContract.errorCode, NOST::EAuctionError::InvalidInput); + + const auto insufficient = nostromo.transferManagedShares(owner, asset, 8, QX_CONTRACT_INDEX); + EXPECT_EQ(insufficient.transferredNumberOfShares, 0); + EXPECT_EQ(insufficient.errorCode, NOST::EAuctionError::InvalidInput); + + const auto success = nostromo.transferManagedShares(owner, asset, 5, QX_CONTRACT_INDEX); + EXPECT_EQ(success.transferredNumberOfShares, 5); + EXPECT_EQ(success.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.managedShares(asset, owner), 2); + EXPECT_EQ(nostromo.sharesManagedBy(asset, owner, QX_CONTRACT_INDEX), 8); +} + +TEST(ContractNostromoAuction, TransferShareManagementRightsRequiresInvocationRewardAuction) +{ + { + ContractTestingNOST nostromo; + const id owner(5, 6, 7, 8); + const uint64 assetName = assetNameFromString("TRFEXA"); + const Asset asset{owner, assetName}; + + EXPECT_EQ(nostromo.issueAsset(owner, assetName, 4), 4); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(owner, asset, 4), 4); + nostromo.syncCachedQxTransferFee(); + + const auto output = nostromo.transferManagedSharesWithReward(owner, asset, 2, QX_CONTRACT_INDEX, nostromo.getCachedQxTransferFee()); + EXPECT_EQ(output.transferredNumberOfShares, 2); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.managedShares(asset, owner), 2); + EXPECT_EQ(nostromo.sharesManagedBy(asset, owner, QX_CONTRACT_INDEX), 2); + } + + { + ContractTestingNOST nostromo; + const id owner(9, 10, 11, 12); + const uint64 assetName = assetNameFromString("TRFINS"); + const Asset asset{owner, assetName}; + + EXPECT_EQ(nostromo.issueAsset(owner, assetName, 4), 4); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(owner, asset, 4), 4); + nostromo.syncCachedQxTransferFee(); + + const auto output = nostromo.transferManagedSharesWithReward(owner, asset, 2, QX_CONTRACT_INDEX, nostromo.getCachedQxTransferFee() - 1); + EXPECT_EQ(output.transferredNumberOfShares, 0); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.managedShares(asset, owner), 4); + EXPECT_EQ(nostromo.sharesManagedBy(asset, owner, QX_CONTRACT_INDEX), 0); + } + + { + ContractTestingNOST nostromo; + const id owner(13, 14, 15, 16); + const uint64 assetName = assetNameFromString("TRFEXC"); + const Asset asset{owner, assetName}; + + EXPECT_EQ(nostromo.issueAsset(owner, assetName, 4), 4); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(owner, asset, 4), 4); + nostromo.syncCachedQxTransferFee(); + const sint64 reward = static_cast(nostromo.getCachedQxTransferFee()) + 50; + nostromo.seedUser(owner, reward); + const sint64 ownerBefore = getBalance(owner); + + const auto output = nostromo.transferManagedSharesWithFundedReward(owner, asset, 2, QX_CONTRACT_INDEX, reward); + EXPECT_EQ(output.transferredNumberOfShares, 2); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(owner) - ownerBefore, -static_cast(nostromo.getCachedQxTransferFee())); + EXPECT_EQ(nostromo.managedShares(asset, owner), 2); + EXPECT_EQ(nostromo.sharesManagedBy(asset, owner, QX_CONTRACT_INDEX), 2); + } + + { + ContractTestingNOST nostromo; + const id owner(17, 18, 19, 20); + const uint64 assetName = assetNameFromString("TRFINV"); + const Asset asset{owner, assetName}; + + EXPECT_EQ(nostromo.issueAsset(owner, assetName, 4), 4); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(owner, asset, 4), 4); + nostromo.syncCachedQxTransferFee(); + + const auto invalidDestination = nostromo.transferManagedSharesWithReward(owner, asset, 2, 0, nostromo.getCachedQxTransferFee()); + EXPECT_EQ(invalidDestination.transferredNumberOfShares, 0); + EXPECT_EQ(invalidDestination.errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.managedShares(asset, owner), 4); + + Asset zeroAsset{}; + const auto zeroAssetOutput = + nostromo.transferManagedSharesWithReward(owner, zeroAsset, 2, QX_CONTRACT_INDEX, nostromo.getCachedQxTransferFee()); + EXPECT_EQ(zeroAssetOutput.transferredNumberOfShares, 0); + EXPECT_EQ(zeroAssetOutput.errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.managedShares(asset, owner), 4); + } +} +TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) +{ + ContractTestingNOST nostromo; + const id seller(11, 12, 13, 14); + const uint64 assetName = assetNameFromString("CRTBTN"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 9), 9); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 9), 9); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 9, 25); + const auto output = nostromo.createAuction(seller, input); + ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(output.auctionIndex, 0ULL); + + const auto auction = nostromo.getAuction(output.auctionIndex).auction; + EXPECT_EQ(auction.core.auctionIndex, output.auctionIndex); + EXPECT_EQ(auction.core.quantityForSale, 9ULL); + EXPECT_EQ(auction.core.minimumPurchaseQuantity, 1ULL); + EXPECT_EQ(auction.core.salePrice, 25ULL); + EXPECT_EQ(auction.core.auctionDurationSeconds, NOST_SECONDS_PER_DAY); + EXPECT_EQ(auction.core.seller, seller); + EXPECT_EQ(auction.core.type, NOST::EAuctionType::Batch); + EXPECT_EQ(auction.core.visibility, NOST::EAuctionVisibility::Public); + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Active); + EXPECT_EQ(auction.core.auctionLotItems.get(0).asset, asset); + EXPECT_EQ(auction.core.auctionLotItems.get(0).quantity, 9); + EXPECT_EQ(auction.core.metadataIpfsCid.get(0), 'b'); + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); + EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 9); +} + +TEST(ContractNostromoAuction, PublicAuctionCreationAccumulatesConfiguredFeeAndRefundsExcessAuction) +{ + ContractTestingNOST nostromo; + const id seller(901, 902, 903, 904); + const Asset asset{seller, assetNameFromString("BCRFEE")}; + constexpr sint64 configuredFee = 73; + const auto feeInput = nostromo.makeCoordinatorFeeInput(configuredFee); + ASSERT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), feeInput).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, configuredFee); + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); + const auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 1, 1); + nostromo.seedUser(seller, 1000); + const sint64 sellerBefore = getBalance(seller); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + uint64 expectedPool = nostromo.getPendingServiceFeePool().pendingServiceFeePool; + + const auto insufficient = nostromo.createAuctionWithFundedReward(seller, input, configuredFee - 1); + EXPECT_EQ(insufficient.errorCode, NOST::EAuctionError::InsufficientFunds); + EXPECT_EQ(getBalance(seller), sellerBefore); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + const auto exact = nostromo.createAuctionWithFundedReward(seller, input, configuredFee); + ASSERT_EQ(exact.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(seller), sellerBefore - configuredFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + configuredFee); + expectedPool += static_cast(configuredFee); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + constexpr sint64 excessReward = configuredFee + 37; + const auto excess = nostromo.createAuctionWithFundedReward(seller, input, excessReward); + ASSERT_EQ(excess.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(seller), sellerBefore - 2 * configuredFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + 2 * configuredFee); + expectedPool += static_cast(configuredFee); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + constexpr sint64 managementConfiguredFee = 29; + const auto managementFeeInput = nostromo.makeManagementFeeInput(managementConfiguredFee); + ASSERT_EQ(nostromo.setAuctionFeesByManagement(ContractTestingNOST::managementWallet(), managementFeeInput).errorCode, + NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, managementConfiguredFee); + const auto managementConfigured = nostromo.createAuctionWithFundedReward(seller, input, managementConfiguredFee); + ASSERT_EQ(managementConfigured.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(seller), sellerBefore - (2 * configuredFee + managementConfiguredFee)); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + 2 * configuredFee + managementConfiguredFee); + expectedPool += static_cast(managementConfiguredFee); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + const id standardSeller(921, 922, 923, 924); + const Asset standardAsset{standardSeller, assetNameFromString("BCFSTD")}; + ASSERT_EQ(nostromo.issueAsset(standardSeller, standardAsset.assetName, 3), 3); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(standardSeller, standardAsset, 3), 3); + const auto standardInput = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(standardAsset, 1)); + nostromo.seedUser(standardSeller, 1000); + const sint64 standardSellerBefore = getBalance(standardSeller); + const sint64 standardContractBefore = getBalance(NOST_CONTRACT_ID); + + const auto standardInsufficient = nostromo.createAuctionWithFundedReward(standardSeller, standardInput, managementConfiguredFee - 1); + EXPECT_EQ(standardInsufficient.errorCode, NOST::EAuctionError::InsufficientFunds); + EXPECT_EQ(getBalance(standardSeller), standardSellerBefore); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), standardContractBefore); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + const auto standardExact = nostromo.createAuctionWithFundedReward(standardSeller, standardInput, managementConfiguredFee); + ASSERT_EQ(standardExact.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(standardSeller), standardSellerBefore - managementConfiguredFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), standardContractBefore + managementConfiguredFee); + expectedPool += static_cast(managementConfiguredFee); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + constexpr sint64 standardExcessReward = managementConfiguredFee + 17; + const auto standardExcess = nostromo.createAuctionWithFundedReward(standardSeller, standardInput, standardExcessReward); + ASSERT_EQ(standardExcess.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(standardSeller), standardSellerBefore - 2 * managementConfiguredFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), standardContractBefore + 2 * managementConfiguredFee); + expectedPool += static_cast(managementConfiguredFee); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + const id privateSeller(925, 926, 927, 928); + const id allowedBidder(929, 930, 931, 932); + const Asset privateAsset{privateSeller, assetNameFromString("BCFPRV")}; + ASSERT_EQ(nostromo.issueAsset(privateSeller, privateAsset.assetName, 1), 1); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(privateSeller, privateAsset, 1), 1); + auto privateInput = ContractTestingNOST::makeBatchAuctionInput(privateAsset, 1, 1); + privateInput.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + privateInput.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); + nostromo.seedUser(privateSeller, NOST_DEFAULT_PRIVATE_AUCTION_FEE + 100); + const sint64 privateSellerBefore = getBalance(privateSeller); + EXPECT_EQ(nostromo.createAuctionWithFundedReward(privateSeller, privateInput, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, + NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(privateSeller), privateSellerBefore - NOST_DEFAULT_PRIVATE_AUCTION_FEE); + expectedPool += static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + const id privateStandardSeller(933, 934, 935, 936); + const Asset privateStandardAsset{privateStandardSeller, assetNameFromString("PRVSTD")}; + ASSERT_EQ(nostromo.issueAsset(privateStandardSeller, privateStandardAsset.assetName, 1), 1); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(privateStandardSeller, privateStandardAsset, 1), 1); + auto privateStandardInput = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(privateStandardAsset, 1)); + privateStandardInput.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + privateStandardInput.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); + nostromo.seedUser(privateStandardSeller, NOST_DEFAULT_PRIVATE_AUCTION_FEE + 100); + const sint64 privateStandardSellerBefore = getBalance(privateStandardSeller); + EXPECT_EQ(nostromo.createAuctionWithFundedReward(privateStandardSeller, privateStandardInput, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, + NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(privateStandardSeller), privateStandardSellerBefore - NOST_DEFAULT_PRIVATE_AUCTION_FEE); + expectedPool += static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, 0ULL); +} + +TEST(ContractNostromoAuction, BatchBidFeeBoundariesAuction) +{ + ContractTestingNOST nostromo; + const struct + { + uint64 bidQuantity; + uint64 bidAmount; + uint64 escrowAmount; + uint64 fee; + uint64 requiredReward; + } cases[] = { + {1, 9, 9, 91, 100}, {1, 10, 10, 90, 100}, {1, 20, 20, 80, 100}, {1, 30, 30, 70, 100}, {10, 100, 1000, 0, 1000}, + {1, 101, 101, 0, 101}, {2, 101, 202, 0, 202}, {2, 19, 38, 62, 100}, {2, 100, 200, 0, 200}, {UINT64_MAX, 2, UINT64_MAX, 0, UINT64_MAX}, + }; + + for (const auto& testCase : cases) + { + SCOPED_TRACE(::testing::Message() << "quantity=" << testCase.bidQuantity << ", bidAmount=" << testCase.bidAmount); + const auto output = nostromo.calculateBatchAuctionBidFee(testCase.bidQuantity, testCase.bidAmount); + EXPECT_EQ(output.escrowAmount, testCase.escrowAmount); + EXPECT_EQ(output.fee, testCase.fee); + EXPECT_EQ(output.requiredReward, testCase.requiredReward); + } +} + +TEST(ContractNostromoAuction, PublicAuctionCreationFeeConfigurationBoundariesAuction) +{ + ContractTestingNOST nostromo; + EXPECT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, NOST_PUBLIC_AUCTION_CREATION_FEE); + + auto coordinatorInput = nostromo.makeCoordinatorFeeInput(0); + ASSERT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, 0LL); + const id zeroFeeSeller(941, 942, 943, 944); + const Asset zeroFeeAsset{zeroFeeSeller, assetNameFromString("ZEROFEE")}; + ASSERT_EQ(nostromo.issueAsset(zeroFeeSeller, zeroFeeAsset.assetName, 1), 1); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(zeroFeeSeller, zeroFeeAsset, 1), 1); + EXPECT_EQ(nostromo.createAuctionWithFundedReward(zeroFeeSeller, ContractTestingNOST::makeBatchAuctionInput(zeroFeeAsset, 1, 1), 0).errorCode, + NOST::EAuctionError::Success); + + coordinatorInput.publicAuctionCreationFee = INT64_MAX; + ASSERT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, INT64_MAX); + + const auto feesBeforeInvalidUpdate = nostromo.getAuctionFees(); + coordinatorInput.publicAuctionCreationFee = -1; + coordinatorInput.auctionCancellationFeeBasisPoints = 0; + EXPECT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput).errorCode, + NOST::EAuctionError::InvalidInput); + const auto feesAfterInvalidUpdate = nostromo.getAuctionFees(); + EXPECT_EQ(feesAfterInvalidUpdate.publicAuctionCreationFee, feesBeforeInvalidUpdate.publicAuctionCreationFee); + EXPECT_EQ(feesAfterInvalidUpdate.auctionCancellationFeeBasisPoints, feesBeforeInvalidUpdate.auctionCancellationFeeBasisPoints); + + auto managementInput = nostromo.makeManagementFeeInput(41); + ASSERT_EQ(nostromo.setAuctionFeesByManagement(ContractTestingNOST::managementWallet(), managementInput).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, 41LL); +} + +TEST(ContractNostromoAuction, AcceptedBatchBidAccumulatesFeeAndKeepsEscrowAuction) +{ + ContractTestingNOST nostromo; + const id seller(905, 906, 907, 908); + const id firstBidder(909, 910, 911, 912); + const Asset asset{seller, assetNameFromString("BBDFEE")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 1)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + nostromo.seedUser(firstBidder, 1000); + const sint64 firstBidderBefore = getBalance(firstBidder); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + const uint64 poolBefore = nostromo.getPendingServiceFeePool().pendingServiceFeePool; + const auto underfundedSmallBid = nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 1, 9, 99); + EXPECT_EQ(underfundedSmallBid.errorCode, NOST::EAuctionError::InsufficientFunds); + EXPECT_EQ(underfundedSmallBid.refundedAmount, 99ULL); + EXPECT_EQ(getBalance(firstBidder), firstBidderBefore); + + const auto calculation = nostromo.calculateBatchAuctionBidFee(2, 40); + ASSERT_EQ(calculation.escrowAmount, 80ULL); + ASSERT_EQ(calculation.fee, 20ULL); + ASSERT_EQ(calculation.requiredReward, 100ULL); + const auto underfunded = nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 2, 40, calculation.requiredReward - 1); + EXPECT_EQ(underfunded.errorCode, NOST::EAuctionError::InsufficientFunds); + EXPECT_EQ(underfunded.refundedAmount, 99ULL); + EXPECT_EQ(getBalance(firstBidder), firstBidderBefore); + + const auto accepted = nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 2, 40, calculation.requiredReward + 49); + ASSERT_EQ(accepted.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(accepted.escrowedAmount, 80ULL); + EXPECT_EQ(accepted.refundedAmount, 49ULL); + EXPECT_EQ(getBalance(firstBidder), firstBidderBefore - 100); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + 100); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, poolBefore + 20ULL); + EXPECT_EQ(nostromo.getNostromoFeePool().feePool.developmentAmount, poolBefore + 20ULL); + + // The contract defaults to routing every fee to development, so the whole accumulated pool (including the earlier creation fee + // already reflected in contractBefore) leaves the contract at END_EPOCH, leaving only the escrowed amount behind. + ASSERT_EQ(nostromo.getRouteAllFeesToDevelopment(), NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT); + nostromo.endEpoch(); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, 0ULL); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + 80 - static_cast(poolBefore)); +} + +TEST(ContractNostromoAuction, CreateStandardSingleAssetAuctionEscrowsLotAuction) +{ + ContractTestingNOST nostromo; + const id seller(21, 22, 23, 24); + const uint64 assetName = assetNameFromString("CRTSTA"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 5), 5); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 5), 5); + + auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 5)); + input.minimumPurchaseQuantity = UINT64_MAX; + + nostromo.seedUser(seller, 1000); + const sint64 sellerBalanceBefore = getBalance(seller); + const sint64 contractBalanceBefore = getBalance(NOST_CONTRACT_ID); + const uint64 poolBefore = nostromo.getPendingServiceFeePool().pendingServiceFeePool; + const auto output = nostromo.createAuctionWithFundedReward(seller, input, NOST_PUBLIC_AUCTION_CREATION_FEE); + ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(seller), sellerBalanceBefore - NOST_PUBLIC_AUCTION_CREATION_FEE); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBalanceBefore + NOST_PUBLIC_AUCTION_CREATION_FEE); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, poolBefore + static_cast(NOST_PUBLIC_AUCTION_CREATION_FEE)); + + const auto auction = nostromo.getAuction(output.auctionIndex).auction; + EXPECT_EQ(auction.core.quantityForSale, 1ULL); + EXPECT_EQ(auction.core.minimumPurchaseQuantity, 0ULL); + EXPECT_EQ(auction.core.initialPrice, NOST_STANDARD_MIN_PRICE); + EXPECT_EQ(auction.core.salePrice, NOST_STANDARD_MIN_PRICE); + EXPECT_EQ(auction.core.minimumBidIncrement, NOST_STANDARD_MIN_BID_INCREMENT); + EXPECT_EQ(auction.core.type, NOST::EAuctionType::Standard); + EXPECT_EQ(auction.core.auctionLotItems.get(0).asset, asset); + EXPECT_EQ(auction.core.auctionLotItems.get(0).quantity, 5); + EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 5); +} + +TEST(ContractNostromoAuction, BatchMinimumPurchaseQuantityCreationBoundsAuction) +{ + ContractTestingNOST nostromo; + const id seller(301, 302, 303, 304); + const Asset asset{seller, assetNameFromString("BATMIN")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 30), 30); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 30), 30); + + auto zeroMinimum = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 10); + zeroMinimum.minimumPurchaseQuantity = 0; + EXPECT_EQ(nostromo.createAuction(seller, zeroMinimum).errorCode, NOST::EAuctionError::InvalidInput); + + auto excessiveMinimum = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 10); + excessiveMinimum.minimumPurchaseQuantity = 11; + EXPECT_EQ(nostromo.createAuction(seller, excessiveMinimum).errorCode, NOST::EAuctionError::InvalidInput); + + auto minimumOne = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 10); + const auto minimumOneOutput = nostromo.createAuction(seller, minimumOne); + ASSERT_EQ(minimumOneOutput.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getAuction(minimumOneOutput.auctionIndex).auction.core.minimumPurchaseQuantity, 1ULL); + + auto fullLotMinimum = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 10); + fullLotMinimum.minimumPurchaseQuantity = 10; + const auto fullLotMinimumOutput = nostromo.createAuction(seller, fullLotMinimum); + ASSERT_EQ(fullLotMinimumOutput.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getAuction(fullLotMinimumOutput.auctionIndex).auction.core.minimumPurchaseQuantity, 10ULL); +} + +TEST(ContractNostromoAuction, BatchBidEnforcesMinimumPurchaseQuantityAndRefundsAuction) +{ + ContractTestingNOST nostromo; + const id seller(305, 306, 307, 308); + const id bidder(309, 310, 311, 312); + const Asset asset{seller, assetNameFromString("BATBIDM")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 15), 15); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 15), 15); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 15, 10); + input.minimumPurchaseQuantity = 10; + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + nostromo.seedUser(bidder, 200); + const sint64 balanceBeforeRejectedBid = getBalance(bidder); + const auto rejectedBid = nostromo.placeBidWithFundedReward(bidder, createOutput.auctionIndex, 9, 10, 90); + EXPECT_EQ(rejectedBid.errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(rejectedBid.refundedAmount, 90ULL); + EXPECT_EQ(rejectedBid.escrowedAmount, 0ULL); + EXPECT_EQ(getBalance(bidder), balanceBeforeRejectedBid); + + const auto acceptedBid = nostromo.placeBatchBidWithFundedRequiredReward(bidder, createOutput.auctionIndex, 10, 10); + EXPECT_EQ(acceptedBid.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(acceptedBid.escrowedAmount, 100ULL); +} + +TEST(ContractNostromoAuction, CreateStandardAuctionSupportsFourLotEntriesAuction) +{ + ContractTestingNOST nostromo; + const id seller(25, 26, 27, 28); + const id bidder(29, 30, 31, 32); + const Asset assets[] = { + {seller, assetNameFromString("MAXLOA")}, + {seller, assetNameFromString("MAXLOB")}, + {seller, assetNameFromString("MAXLOC")}, + {seller, assetNameFromString("MAXLOD")}, + }; + + EXPECT_EQ(NOST_BATCH_AUCTION_LOT_ITEM_NUM, 1ULL); + EXPECT_EQ(NOST_AUCTION_LOT_ITEM_NUM, 4); + for (const auto& asset : assets) + { + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); + } + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeLot( + {{assets[0], 3}, {assets[1], 3}, {assets[2], 3}, {assets[3], 3}}))); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + for (const auto& asset : assets) + { + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); + EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 3); + } + + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, + NOST::EAuctionError::Success); + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + for (const auto& asset : assets) + { + EXPECT_EQ(nostromo.managedShares(asset, bidder), 3); + EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 0); + } +} + +TEST(ContractNostromoAuction, CreateBatchAuctionRejectsMultipleLotEntriesAuction) +{ + ContractTestingNOST nostromo; + const id seller(33, 34, 35, 36); + const Asset firstAsset{seller, assetNameFromString("BATLOA")}; + const Asset secondAsset{seller, assetNameFromString("BATLOB")}; + + EXPECT_EQ(nostromo.issueAsset(seller, firstAsset.assetName, 2), 2); + EXPECT_EQ(nostromo.issueAsset(seller, secondAsset.assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, firstAsset, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, secondAsset, 2), 2); + + auto input = ContractTestingNOST::makeBatchAuctionInput(firstAsset, 2); + input.auctionLotItems = ContractTestingNOST::makeLot({{firstAsset, 2}, {secondAsset, 2}}); + + EXPECT_EQ(nostromo.createAuction(seller, input).errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.managedShares(firstAsset, seller), 2); + EXPECT_EQ(nostromo.managedShares(secondAsset, seller), 2); +} + +TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(31, 32, 33, 34); + const id allowedBidder(35, 36, 37, 38); + const uint64 assetName = assetNameFromString("PRIWAL"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 4), 4); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 4), 4); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 4, 12); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); + + const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); + + const auto auction = nostromo.getAuction(output.auctionIndex).auction; + EXPECT_EQ(auction.core.visibility, NOST::EAuctionVisibility::Private); + EXPECT_EQ(auction.allowedBidderWalletCount, 1U); + EXPECT_EQ(auction.allowedBidderWallets.get(0), allowedBidder); + EXPECT_EQ(auction.requiredAccessAssetCount, 0U); + } + + { + ContractTestingNOST nostromo; + const id seller(41, 42, 43, 44); + const id gatedBidder(45, 46, 47, 48); + const uint64 saleAssetName = assetNameFromString("PRIACC"); + const uint64 gateAssetName = assetNameFromString("GATEAS"); + const Asset saleAsset{seller, saleAssetName}; + const Asset gateAsset{gatedBidder, gateAssetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, saleAssetName, 5), 5); + EXPECT_EQ(nostromo.issueAsset(gatedBidder, gateAssetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, saleAsset, 5), 5); + + auto input = ContractTestingNOST::makeBatchAuctionInput(saleAsset, 5, 20); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{gateAsset, 1}}); + + const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); + + const auto auction = nostromo.getAuction(output.auctionIndex).auction; + EXPECT_EQ(auction.core.visibility, NOST::EAuctionVisibility::Private); + EXPECT_EQ(auction.allowedBidderWalletCount, 0U); + EXPECT_EQ(auction.requiredAccessAssetCount, 1U); + EXPECT_EQ(auction.requiredAccessAssets.get(0).asset, gateAsset); + EXPECT_EQ(auction.requiredAccessAssets.get(0).quantity, 1); + EXPECT_GT(nostromo.plainShares(gateAsset, gatedBidder), 0); + } +} + +TEST(ContractNostromoAuction, GetAuctionViewExposesAccessListsAndFoundFlagAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(45, 46, 47, 48); + const id walletA(49, 50, 51, 52); + const id walletB(53, 54, 55, 56); + const uint64 assetName = assetNameFromString("VIEWWL"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({walletA, walletB}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto auctionOutput = nostromo.getAuction(createOutput.auctionIndex); + EXPECT_EQ(auctionOutput.found, 1); + EXPECT_EQ(auctionOutput.auction.core.auctionIndex, createOutput.auctionIndex); + EXPECT_EQ(auctionOutput.auction.allowedBidderWalletCount, 2U); + EXPECT_TRUE(containsWallet(auctionOutput.auction.allowedBidderWallets, auctionOutput.auction.allowedBidderWalletCount, walletA)); + EXPECT_TRUE(containsWallet(auctionOutput.auction.allowedBidderWallets, auctionOutput.auction.allowedBidderWalletCount, walletB)); + EXPECT_EQ(auctionOutput.auction.requiredAccessAssetCount, 0U); + } + + { + ContractTestingNOST nostromo; + const id seller(57, 58, 59, 60); + const id gateIssuerA(61, 62, 63, 64); + const id gateIssuerB(65, 66, 67, 68); + const uint64 assetName = assetNameFromString("VIEWAC"); + const Asset asset{seller, assetName}; + const Asset accessAssetA{gateIssuerA, assetNameFromString("GATEA1")}; + const Asset accessAssetB{gateIssuerB, assetNameFromString("GATEB1")}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.requiredAccessAssets = + ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAssetA, 2}, NOST::AuctionAssetEntry{accessAssetB, 5}}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto auctionOutput = nostromo.getAuction(createOutput.auctionIndex); + EXPECT_EQ(auctionOutput.found, 1); + EXPECT_EQ(auctionOutput.auction.requiredAccessAssetCount, 2U); + EXPECT_TRUE(containsAccessAsset(auctionOutput.auction.requiredAccessAssets, auctionOutput.auction.requiredAccessAssetCount, + NOST::AuctionAssetEntry{accessAssetA, 2})); + EXPECT_TRUE(containsAccessAsset(auctionOutput.auction.requiredAccessAssets, auctionOutput.auction.requiredAccessAssetCount, + NOST::AuctionAssetEntry{accessAssetB, 5})); + EXPECT_EQ(auctionOutput.auction.allowedBidderWalletCount, 0U); + } + + { + ContractTestingNOST nostromo; + const uint64 missingAuction = 999; + const auto auctionOutput = nostromo.getAuction(missingAuction); + EXPECT_EQ(auctionOutput.found, 0); + EXPECT_EQ(auctionOutput.auction.core.auctionIndex, 0ULL); + } +} + +TEST(ContractNostromoAuction, GetAuctionViewDeduplicatesPrivateAccessInputsAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(69, 70, 71, 72); + const id wallet(73, 74, 75, 76); + const uint64 assetName = assetNameFromString("DUPWAL"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({wallet, wallet}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.allowedBidderWalletCount, 1U); + EXPECT_TRUE(containsWallet(auction.allowedBidderWallets, auction.allowedBidderWalletCount, wallet)); + } + + { + ContractTestingNOST nostromo; + const id seller(77, 78, 79, 80); + const id gateIssuer(81, 82, 83, 84); + const uint64 assetName = assetNameFromString("DUPACC"); + const Asset asset{seller, assetName}; + const Asset accessAsset{gateIssuer, assetNameFromString("GATEDP")}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets( + {NOST::AuctionAssetEntry{accessAsset, 2}, NOST::AuctionAssetEntry{accessAsset, 5}, NOST::AuctionAssetEntry{accessAsset, 3}}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.requiredAccessAssetCount, 1U); + EXPECT_TRUE(containsAccessAsset(auction.requiredAccessAssets, auction.requiredAccessAssetCount, NOST::AuctionAssetEntry{accessAsset, 5})); + } +} + +TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacityAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(47, 48, 49, 50); + const id allowedBidder(30007, 31007, 32007, 33007); + const uint64 assetName = assetNameFromString("MAXWAL"); + const Asset asset{seller, assetName}; + Array allowedWallets{}; + EXPECT_EQ(NOST_AUCTION_ALLOWED_WALLET_NUM, 16ULL); + + for (uint64 index = 0; index < NOST_AUCTION_ALLOWED_WALLET_NUM; ++index) + { + allowedWallets.set(index, id(30000 + index, 31000 + index, 32000 + index, 33000 + index)); + } + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = allowedWallets; + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto auctionOutput = nostromo.getAuction(createOutput.auctionIndex); + EXPECT_EQ(auctionOutput.auction.allowedBidderWalletCount, NOST_AUCTION_ALLOWED_WALLET_NUM); + EXPECT_TRUE(containsWallet(auctionOutput.auction.allowedBidderWallets, auctionOutput.auction.allowedBidderWalletCount, allowedBidder)); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(allowedBidder, createOutput.auctionIndex, 1, 10).errorCode, NOST::EAuctionError::Success); + } + + { + ContractTestingNOST nostromo; + const id seller(57, 58, 59, 60); + const id accessBidder(61, 62, 63, 64); + const id gateIssuer(65, 66, 67, 68); + const uint64 saleAssetName = assetNameFromString("MAXACC"); + const uint64 bidderAccessAssetName = assetNameFromString("MAXACB"); + const Asset saleAsset{seller, saleAssetName}; + Array requiredAssets{}; + + for (uint64 index = 0; index < NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM; ++index) + { + requiredAssets.set(index, NOST::AuctionAssetEntry{Asset{gateIssuer, 34000 + index}, 1}); + } + requiredAssets.set(NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM - 1, NOST::AuctionAssetEntry{Asset{accessBidder, bidderAccessAssetName}, 1}); + + EXPECT_EQ(nostromo.issueAsset(seller, saleAssetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, saleAsset, 1), 1); + EXPECT_EQ(nostromo.issueAsset(accessBidder, bidderAccessAssetName, 1), 1); + + auto input = ContractTestingNOST::makeBatchAuctionInput(saleAsset, 1, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.requiredAccessAssets = requiredAssets; + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto auctionOutput = nostromo.getAuction(createOutput.auctionIndex); + EXPECT_EQ(auctionOutput.auction.requiredAccessAssetCount, NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM); + EXPECT_TRUE(containsAccessAsset(auctionOutput.auction.requiredAccessAssets, auctionOutput.auction.requiredAccessAssetCount, + NOST::AuctionAssetEntry{Asset{accessBidder, bidderAccessAssetName}, 1})); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(accessBidder, createOutput.auctionIndex, 1, 10).errorCode, NOST::EAuctionError::Success); + } +} + +TEST(ContractNostromoAuction, PrivateAuctionFeeIsDistributedAcrossRecipientsAuction) +{ + const uint8 routeModes[] = {0, 1}; + for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) + { + SCOPED_TRACE(::testing::Message() << "routeAllFeesToDevelopment=" << static_cast(routeModes[routeIndex])); + ContractTestingNOST nostromo; + const uint8 routeMode = routeModes[routeIndex]; + const id seller(61 + routeIndex, 62 + routeIndex, 63 + routeIndex, 64 + routeIndex); + const id allowedBidder(71 + routeIndex, 72 + routeIndex, 73 + routeIndex, 74 + routeIndex); + const uint64 assetName = assetNameFromString(routeMode ? "STDENR1" : "STDENR0"); + + const Asset asset{seller, assetName}; + + nostromo.setRouteAllFeesToDevelopment(routeMode); + EXPECT_EQ(nostromo.getRouteAllFeesToDevelopment(), routeMode); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 4), 4); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 4), 4); + + const sint64 sellerBefore = getBalance(seller); + const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + constexpr uint64 expectedShareholderDividend = 36350000ULL; + constexpr uint64 expectedManagementFee = 4550000ULL; + constexpr uint64 expectedDevelopmentFee = 4550000ULL; + constexpr uint64 expectedCoordinatorFee = 4550000ULL; + const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedShareholderDividend); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 4, 12); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); + + const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); + + EXPECT_EQ(getBalance(seller) - sellerBefore, 0); + + // The fee accumulates in the pool and is not distributed until END_EPOCH, regardless of the route-to-development mode. + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE)); + const auto pendingFeePool = nostromo.getNostromoFeePool(); + EXPECT_EQ(pendingFeePool.totalAmount, static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE)); + EXPECT_EQ(pendingFeePool.feePool.commonServiceFeeAmount, routeMode == 0 ? static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE) : 0ULL); + EXPECT_EQ(pendingFeePool.feePool.developmentAmount, routeMode != 0 ? static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE) : 0ULL); + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, 0ULL); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 0ULL); + + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); + } + else { - if (tmpInvestedList.get(i).indexOfFundraising == indexOfFundraising) + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedManagementFee); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedDevelopmentFee); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedCoordinatorFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); + } + } +} + +TEST(ContractNostromoAuction, EndEpochUsesCurrentManagementWalletForAccruedFeesAuction) +{ + ContractTestingNOST nostromo; + const id seller(1501, 1502, 1503, 1504); + const id allowedBidder(1505, 1506, 1507, 1508); + const id newManagement(1509, 1510, 1511, 1512); + const Asset asset{seller, assetNameFromString("CURMGR")}; + + nostromo.setRouteAllFeesToDevelopment(0); + ASSERT_EQ(nostromo.issueAsset(seller, asset.assetName, 1), 1); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); + ASSERT_EQ(nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, NOST::EAuctionError::Success); + + nostromo.ensureUser(newManagement); + const sint64 previousManagementBefore = getBalance(ContractTestingNOST::managementWallet()); + const sint64 newManagementBefore = getBalance(newManagement); + ASSERT_EQ(nostromo.setManagement(ContractTestingNOST::takeoverCoordinatorWallet(), newManagement).errorCode, NOST::EAuctionError::Success); + + nostromo.endEpoch(); + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()), previousManagementBefore); + EXPECT_EQ(getBalance(newManagement) - newManagementBefore, 4550000ULL); +} + +TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) +{ + ContractTestingNOST nostromo; + const id seller(51, 52, 53, 54); + const id altIssuer(55, 56, 57, 58); + const uint64 assetNameA = assetNameFromString("INVAAA"); + const Asset assetA{seller, assetNameA}; + const Asset accessAsset{altIssuer, assetNameFromString("GATINV")}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetNameA, 5), 5); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetA, 5), 5); + EXPECT_EQ(nostromo.issueAsset(altIssuer, assetNameFromString("GATINV"), 1), 1); + nostromo.seedUser(seller, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + const sint64 sellerBalanceBeforeInvalidCalls = getBalance(seller); + const sint64 contractBalanceBeforeInvalidCalls = getBalance(NOST_CONTRACT_ID); + const auto invokeRejectedPublicAuction = [&nostromo, &seller](const NOST::CreateAuction_input& input) { + return nostromo.createAuctionWithFundedReward(seller, input, NOST_PUBLIC_AUCTION_CREATION_FEE); + }; + const auto invokeRejectedPrivateAuction = [&nostromo, &seller](const NOST::CreateAuction_input& input) { + return nostromo.createAuctionWithFundedReward(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + }; + + auto invalidCid = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + invalidCid.metadataIpfsCid = ContractTestingNOST::makeInvalidMetadataCidFirstChar(); + EXPECT_EQ(invokeRejectedPublicAuction(invalidCid).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidCidUppercase = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + invalidCidUppercase.metadataIpfsCid = ContractTestingNOST::makeInvalidMetadataCidUppercase(); + EXPECT_EQ(invokeRejectedPublicAuction(invalidCidUppercase).errorCode, NOST::EAuctionError::InvalidInput); + + auto emptyLot = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + emptyLot.auctionLotItems = Array{}; + EXPECT_EQ(invokeRejectedPublicAuction(emptyLot).errorCode, NOST::EAuctionError::InvalidInput); + + auto negativeQuantity = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + negativeQuantity.auctionLotItems = ContractTestingNOST::makeSingleLot(assetA, -1); + EXPECT_EQ(invokeRejectedPublicAuction(negativeQuantity).errorCode, NOST::EAuctionError::InvalidInput); + + auto zeroDuration = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + zeroDuration.durationDays = 0; + EXPECT_EQ(invokeRejectedPublicAuction(zeroDuration).errorCode, NOST::EAuctionError::InvalidInput); + + auto tooLongDuration = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + tooLongDuration.durationDays = NOST_AUCTION_MAX_DURATION_DAYS + 1; + EXPECT_EQ(invokeRejectedPublicAuction(tooLongDuration).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidType = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + invalidType.auctionType = 99; + EXPECT_EQ(invokeRejectedPublicAuction(invalidType).errorCode, NOST::EAuctionError::InvalidAuctionType); + + auto invalidVisibility = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + invalidVisibility.auctionVisibility = 99; + EXPECT_EQ(invokeRejectedPublicAuction(invalidVisibility).errorCode, NOST::EAuctionError::InvalidVisibility); + + auto partiallyEmptyLot = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + partiallyEmptyLot.auctionLotItems = ContractTestingNOST::makeSingleLot(Asset{}, 1); + EXPECT_EQ(invokeRejectedPublicAuction(partiallyEmptyLot).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidBatchBuyNow = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + invalidBatchBuyNow.buyNowPrice = 100; + EXPECT_EQ(invokeRejectedPublicAuction(invalidBatchBuyNow).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidStandardIncrement = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); + invalidStandardIncrement.minimumBidIncrement = 0; + EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardIncrement).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidStandardLowInitial = ContractTestingNOST::makeStandardAuctionInput( + ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE - 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_BID_INCREMENT); + EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardLowInitial).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidStandardLowSale = ContractTestingNOST::makeStandardAuctionInput( + ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE - 1, NOST_STANDARD_MIN_BID_INCREMENT); + EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardLowSale).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidStandardLowIncrement = ContractTestingNOST::makeStandardAuctionInput( + ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_BID_INCREMENT - 1); + EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardLowIncrement).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidStandardPrice = ContractTestingNOST::makeStandardAuctionInput( + ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE + 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_BID_INCREMENT); + EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardPrice).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidStandardSalePrice = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); + invalidStandardSalePrice.salePrice = 0; + EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardSalePrice).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidStandardBuyNow = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE, + NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT, + NOST_STANDARD_MIN_BID_INCREMENT, NOST_STANDARD_MIN_PRICE - 1); + EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardBuyNow).errorCode, NOST::EAuctionError::InvalidInput); + + auto privateWithoutGate = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + privateWithoutGate.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + EXPECT_EQ(invokeRejectedPrivateAuction(privateWithoutGate).errorCode, NOST::EAuctionError::InvalidInput); + + auto zeroAccessQuantity = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + zeroAccessQuantity.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + zeroAccessQuantity.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAsset, 0}}); + EXPECT_EQ(invokeRejectedPrivateAuction(zeroAccessQuantity).errorCode, NOST::EAuctionError::InvalidInput); + + auto negativeAccessQuantity = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + negativeAccessQuantity.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + negativeAccessQuantity.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAsset, -1}}); + EXPECT_EQ(invokeRejectedPrivateAuction(negativeAccessQuantity).errorCode, NOST::EAuctionError::InvalidInput); + + auto partiallyEmptyAccessAsset = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + partiallyEmptyAccessAsset.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + partiallyEmptyAccessAsset.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{Asset{}, 1}}); + EXPECT_EQ(invokeRejectedPrivateAuction(partiallyEmptyAccessAsset).errorCode, NOST::EAuctionError::InvalidInput); + + EXPECT_EQ(getBalance(seller), sellerBalanceBeforeInvalidCalls); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBalanceBeforeInvalidCalls); + EXPECT_EQ(nostromo.managedShares(assetA, seller), 5); + EXPECT_EQ(nostromo.getLatestAuctionIndex().found, 0); + EXPECT_EQ(nostromo.getContractStats().stats.totalAuctionsCreated, 0ULL); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 0ULL); +} + +TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientAssetBalanceAndPauseAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(61, 62, 63, 64); + const uint64 assetName = assetNameFromString("PRIFEE"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 4), 4); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 4), 4); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 4, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({id(1, 1, 1, 1)}); + + const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE - 1); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::InsufficientFunds); + EXPECT_EQ(nostromo.managedShares(asset, seller), 4); + } + + { + ContractTestingNOST nostromo; + const id seller(71, 72, 73, 74); + const uint64 assetName = assetNameFromString("BALLOW"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 3, 10); + const auto output = nostromo.createAuction(seller, input); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::InsufficientAssetBalance); + EXPECT_EQ(nostromo.managedShares(asset, seller), 2); + } + + { + ContractTestingNOST nostromo; + const id seller(81, 82, 83, 84); + const uint64 assetName = assetNameFromString("PAUSEA"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 3, 10); + nostromo.setNow(2026, 1, 7, 11, 40, 0); + nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + + const auto output = nostromo.createAuction(seller, input); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::AuctionPaused); + EXPECT_EQ(output.auctionIndex, 0ULL); + EXPECT_EQ(nostromo.managedShares(asset, seller), 3); + } + + { + ContractTestingNOST nostromo; + const id seller(85, 86, 87, 88); + const uint64 assetName = assetNameFromString("BOOTPA"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 3, 10); + nostromo.setNow(2022, 4, 13, 12, 0, 0); + nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + + const auto output = nostromo.createAuction(seller, input); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::AuctionPaused); + EXPECT_EQ(output.auctionIndex, 0ULL); + EXPECT_EQ(nostromo.managedShares(asset, seller), 3); + } +} + +TEST(ContractNostromoAuction, CreateAuctionRejectsWhenAuctionStorageIsFullAuction) +{ + ContractTestingNOST nostromo; + const id seller(87, 88, 89, 90); + const uint64 assetName = assetNameFromString("STOFUL"); + const Asset asset{seller, assetName}; + + for (uint64 index = 0; index < NOST_AUCTION_NUM; ++index) + { + NOST::AuctionData auction{}; + auction.core.auctionIndex = index; + auction.core.seller = seller; + auction.core.status = NOST::EAuctionStatus::Active; + ASSERT_NE(nostromo.stateData().auctionList.set(auction.core.auctionIndex, auction), NULL_INDEX); + } + ASSERT_EQ(nostromo.stateData().auctionList.population(), NOST_AUCTION_NUM); + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto output = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10)); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::StorageFull); + EXPECT_EQ(output.auctionIndex, 0ULL); + EXPECT_EQ(nostromo.managedShares(asset, seller), 1); +} + +TEST(ContractNostromoAuction, CreateAuctionRejectsWhenAuctionIndexIsExhaustedAuction) +{ + ContractTestingNOST nostromo; + const id seller(89, 90, 91, 92); + const uint64 assetName = assetNameFromString("IDXMAX"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + nostromo.stateData().totalAuctionsCreated = UINT64_MAX; + + const auto output = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10)); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::AuctionIndexExhausted); + EXPECT_EQ(output.auctionIndex, 0ULL); + EXPECT_EQ(nostromo.stateData().totalAuctionsCreated, UINT64_MAX); + EXPECT_EQ(nostromo.stateData().auctionList.population(), 0ULL); + EXPECT_EQ(nostromo.managedShares(asset, seller), 1); +} + +TEST(ContractNostromoAuction, PlaceBidRejectsWhenParticipantStorageIsFullAuction) +{ + ContractTestingNOST nostromo; + const id seller(91, 92, 93, 94); + const id bidder(95, 96, 97, 98); + const uint64 assetName = assetNameFromString("PARFUL"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + for (uint64 index = 0; index < NOST_AUCTION_PARTICIPANT_NUM; ++index) + { + NOST::AuctionParticipantData participant{}; + participant.auctionIndex = index + 100000ULL; + participant.bidIndex = index; + participant.participant = id(14000 + index, 15000 + index, 16000 + index, 17000 + index); + participant.bidAmount = 1; + participant.requestedQuantity = 1; + participant.isUsed = 1; + participant.isActive = 1; + nostromo.stateData().participants.set(index, participant); + } + uint64 usedParticipantCount = 0; + for (uint64 index = 0; index < NOST_AUCTION_PARTICIPANT_NUM; ++index) + { + if (nostromo.stateData().participants.get(index).isUsed) + { + ++usedParticipantCount; + } + } + ASSERT_EQ(usedParticipantCount, NOST_AUCTION_PARTICIPANT_NUM); + + const auto output = nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, 1, 10); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::StorageFull); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.highestBidAmount, 0ULL); + EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidder).found, 0); +} + +TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAuction) +{ + ContractTestingNOST nostromo; + const id seller(91, 92, 93, 94); + const id bidderA(95, 96, 97, 98); + const id bidderB(99, 100, 101, 102); + const id bidderC(103, 104, 105, 106); + const uint64 assetName = assetNameFromString("BIDBAT"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 6), 6); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 6), 6); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 6, 10)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto sellerBid = nostromo.placeBid(seller, createOutput.auctionIndex, 1, 12, 12); + EXPECT_EQ(sellerBid.errorCode, NOST::EAuctionError::Forbidden); + + const auto missingAuction = nostromo.placeBid(bidderA, 700, 1, 12, 12); + EXPECT_EQ(missingAuction.errorCode, NOST::EAuctionError::AuctionNotFound); + + const auto zeroQuantity = nostromo.placeBid(bidderA, createOutput.auctionIndex, 0, 12, 12); + EXPECT_EQ(zeroQuantity.errorCode, NOST::EAuctionError::InvalidInput); + + const auto zeroBid = nostromo.placeBid(bidderA, createOutput.auctionIndex, 1, 0, 1); + EXPECT_EQ(zeroBid.errorCode, NOST::EAuctionError::InvalidInput); + + const auto tooLow = nostromo.placeBid(bidderA, createOutput.auctionIndex, 1, 9, 9); + EXPECT_EQ(tooLow.errorCode, NOST::EAuctionError::BidTooLow); + + const auto insufficientFunds = nostromo.placeBid(bidderA, createOutput.auctionIndex, 2, 12, 23); + EXPECT_EQ(insufficientFunds.errorCode, NOST::EAuctionError::InsufficientFunds); + + const auto bidA1 = nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 2, 20); + const auto bidB = nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 3, 15); + ASSERT_EQ(bidA1.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(bidB.errorCode, NOST::EAuctionError::Success); + + auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.highestBidder, bidderA); + EXPECT_EQ(auction.core.highestBidPrice, 20ULL); + EXPECT_EQ(auction.core.highestBidAmount, 40ULL); + + const auto bidA2 = nostromo.placeBid(bidderA, createOutput.auctionIndex, 2, 14, 28); + EXPECT_EQ(bidA2.errorCode, NOST::EAuctionError::QuantityUnavailable); + EXPECT_EQ(bidA2.refundedAmount, 28ULL); + + auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.highestBidder, bidderA); + EXPECT_EQ(auction.core.highestBidPrice, 20ULL); + EXPECT_EQ(auction.core.highestBidAmount, 40ULL); + + const auto participantA = nostromo.getParticipant(createOutput.auctionIndex, bidderA); + ASSERT_EQ(participantA.found, 1); + EXPECT_EQ(participantA.participantData.escrowedAmount, 40ULL); + EXPECT_EQ(participantA.participantData.bidAmount, 20ULL); + + nostromo.setNow(2026, 1, 2, 9, 0, 1); + const auto closed = nostromo.placeBid(bidderC, createOutput.auctionIndex, 1, 30, 30); + EXPECT_EQ(closed.errorCode, NOST::EAuctionError::AuctionClosed); +} + +TEST(ContractNostromoAuction, PlaceBatchBidExtendsAuctionNearEndAuction) +{ + ContractTestingNOST nostromo; + const id seller(111, 112, 113, 114); + const id bidder(115, 116, 117, 118); + const uint64 assetName = assetNameFromString("BIDEXT"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + nostromo.setNow(2026, 1, 2, 8, 56, 30); + const auto bidOutput = nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, 1, 15); + ASSERT_EQ(bidOutput.errorCode, NOST::EAuctionError::Success); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.auctionDurationSeconds, NOST_SECONDS_PER_DAY + NOST_AUCTION_EXTENSION_SECONDS); +} + +TEST(ContractNostromoAuction, BatchBidAvailabilityRejectsOversizedTailAuction) +{ + ContractTestingNOST nostromo; + const id seller(601, 602, 603, 604); + const id bidderA(605, 606, 607, 608); + const id bidderB(609, 610, 611, 612); + const Asset asset{seller, assetNameFromString("BAVAIL")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 8, 40).errorCode, NOST::EAuctionError::Success); + auto availability = nostromo.getBatchAvailability(createOutput.auctionIndex); + EXPECT_EQ(availability.found, 1); + EXPECT_EQ(availability.isAcceptingBids, 1); + EXPECT_EQ(availability.minimumBidPrice, 2ULL); + EXPECT_EQ(availability.availableQuantity, 2ULL); + + const auto oversized = nostromo.placeBid(bidderB, createOutput.auctionIndex, 3, 20, 6); + EXPECT_EQ(oversized.errorCode, NOST::EAuctionError::QuantityUnavailable); + EXPECT_EQ(oversized.refundedAmount, 6ULL); + EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidderB).found, 0); + + const auto exactTail = nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 2, 20); + EXPECT_EQ(exactTail.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidderB).participantData.requestedQuantity, 2ULL); +} + +TEST(ContractNostromoAuction, BatchCoveredLotRequiresHigherPriceAuction) +{ + ContractTestingNOST nostromo; + const id seller(613, 614, 615, 616); + const id bidderA(617, 618, 619, 620); + const id bidderB(621, 622, 623, 624); + const Asset asset{seller, assetNameFromString("BCOVER")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 10, 30).errorCode, NOST::EAuctionError::Success); + auto availability = nostromo.getBatchAvailability(createOutput.auctionIndex); + EXPECT_EQ(availability.minimumBidPrice, 31ULL); + EXPECT_EQ(availability.availableQuantity, 0ULL); + EXPECT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, 20, 2).errorCode, NOST::EAuctionError::BidTooLow); + EXPECT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, 30, 3).errorCode, NOST::EAuctionError::BidTooLow); + + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 3, 40).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidderA).participantData.requestedQuantity, 7ULL); + EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidderB).participantData.requestedQuantity, 3ULL); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidderA), 7); + EXPECT_EQ(nostromo.managedShares(asset, bidderB), 3); + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); +} + +TEST(ContractNostromoAuction, BatchMinimumPurchaseTailAndSameBidderDisplacementAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(625, 626, 627, 628); + const id bidderA(629, 630, 631, 632); + const id bidderB(633, 634, 635, 636); + const Asset asset{seller, assetNameFromString("BTAILM")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2); + input.minimumPurchaseQuantity = 3; + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 8, 40).errorCode, NOST::EAuctionError::Success); + auto availability = nostromo.getBatchAvailability(createOutput.auctionIndex); + EXPECT_EQ(availability.minimumBidPrice, 41ULL); + EXPECT_EQ(availability.availableQuantity, 0ULL); + EXPECT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 2, 4, 8).errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 3, 40, 12).errorCode, NOST::EAuctionError::BidTooLow); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 3, 50).errorCode, NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidderA), 7); + EXPECT_EQ(nostromo.managedShares(asset, bidderB), 3); + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); + } + + { + ContractTestingNOST nostromo; + const id seller(637, 638, 639, 640); + const id bidderA(641, 642, 643, 644); + const Asset asset{seller, assetNameFromString("BSAMEB")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2); + input.minimumPurchaseQuantity = 3; + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 10, 30).errorCode, NOST::EAuctionError::Success); + const auto improved = nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 8, 40); + EXPECT_EQ(improved.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(improved.refundedAmount, 300ULL); + + const auto participants = nostromo.getAuctionParticipants(createOutput.auctionIndex, 0, 64); + ASSERT_EQ(participants.totalCount, 2ULL); + uint64 quantityAtThirty = 0; + uint64 quantityAtForty = 0; + for (uint64 index = 0; index < participants.returnedCount; ++index) + { + if (participants.participants.get(index).bidAmount == 30) { - return tmpInvestedList.get(i).investedAmount; + quantityAtThirty = participants.participants.get(index).requestedQuantity; } + if (participants.participants.get(index).bidAmount == 40) + { + quantityAtForty = participants.participants.get(index).requestedQuantity; + } + } + EXPECT_EQ(quantityAtThirty, 0ULL); + EXPECT_EQ(quantityAtForty, 8ULL); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidderA), 8); + EXPECT_EQ(nostromo.managedShares(asset, seller), 2); + } +} + +TEST(ContractNostromoAuction, BatchDisplacementKeepsExactMinimumResidualAuction) +{ + ContractTestingNOST nostromo; + const id seller(6601, 6602, 6603, 6604); + const id bidderA(6611, 6612, 6613, 6614); + const id bidderB(6621, 6622, 6623, 6624); + const Asset asset{seller, assetNameFromString("BMINEX")}; + + ASSERT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2); + input.minimumPurchaseQuantity = 3; + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 6, 30).errorCode, NOST::EAuctionError::Success); + const auto higherBid = nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 7, 40); + ASSERT_EQ(higherBid.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(higherBid.refundedAmount, 90ULL); + EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidderA).participantData.requestedQuantity, 3ULL); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidderA), 3); + EXPECT_EQ(nostromo.managedShares(asset, bidderB), 7); + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); +} + +TEST(ContractNostromoAuction, DeterministicBatchAllocationPropertiesAuction) +{ + uint64 generatorState = 0x9E3779B97F4A7C15ULL; + // Unsigned wraparound is intentional: this fixed LCG makes boundary-heavy scenarios reproducible. + for (uint64 scenario = 0; scenario < 12; ++scenario) + { + SCOPED_TRACE(::testing::Message() << "scenario=" << scenario); + ContractTestingNOST nostromo; + generatorState = generatorState * 6364136223846793005ULL + 1442695040888963407ULL; + const uint64 quantityForSale = 3ULL + generatorState % 6ULL; + generatorState = generatorState * 6364136223846793005ULL + 1442695040888963407ULL; + const uint64 minimumPurchaseQuantity = 1ULL + generatorState % quantityForSale; + const id seller(7000 + scenario, 7100 + scenario, 7200 + scenario, 7300 + scenario); + const Asset asset{seller, assetNameFromString("PROPBA")}; + + ASSERT_EQ(nostromo.issueAsset(seller, asset.assetName, static_cast(quantityForSale)), static_cast(quantityForSale)); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, static_cast(quantityForSale)), + static_cast(quantityForSale)); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, static_cast(quantityForSale), 10); + input.minimumPurchaseQuantity = minimumPurchaseQuantity; + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + for (uint64 bidIndex = 0; bidIndex < 6; ++bidIndex) + { + SCOPED_TRACE(::testing::Message() << "bidIndex=" << bidIndex); + generatorState = generatorState * 6364136223846793005ULL + 1442695040888963407ULL; + const uint64 bidQuantity = minimumPurchaseQuantity + generatorState % (quantityForSale - minimumPurchaseQuantity + 1ULL); + const uint64 bidPrice = 20ULL + bidIndex * 10ULL; + const id bidder(8000 + scenario * 10 + bidIndex, 9000 + bidIndex, 10000 + scenario, 11000 + bidIndex); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, bidQuantity, bidPrice).errorCode, + NOST::EAuctionError::Success); } - return 0; - } - uint64 getEpochRevenue() - { - return epochRevenue; - } - void totalRaisedFundChecker(uint32 indexOfFundraising, uint64 raisedFund, uint64 assetName) - { - EXPECT_EQ(raisedFund, fundaraisings.get(indexOfFundraising).raisedFunds); - - if (fundaraisings.get(indexOfFundraising).isCreatedToken) - { - Asset assetInfo; - assetInfo.assetName = assetName; - assetInfo.issuer = id(NOST_CONTRACT_INDEX, 0, 0, 0); - EXPECT_EQ(numberOfShares(assetInfo), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).supplyOfToken); - EXPECT_EQ(numberOfPossessedShares(assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator, projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).supplyOfToken - fundaraisings.get(indexOfFundraising).soldAmount); - } - } - void endEpochSucceedFundraisingChecker(id creator, uint32 indexOfFundraising, uint64 totalInvestedFund, uint64 originalCreatorBalance, uint64 assetName) - { - EXPECT_EQ(numberOfPossessedShares(assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), creator, creator, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).supplyOfToken - div(totalInvestedFund, fundaraisings.get(indexOfFundraising).tokenPrice)); - EXPECT_EQ(fundaraisings.get(indexOfFundraising).raisedFunds, 0); - } - void endEpochFailedFundraisingChecker(uint32 indexOfFundraising) - { - EXPECT_EQ(fundaraisings.get(indexOfFundraising).raisedFunds, 0); - } - void endEpochVoteStatusClearChecker() - { - id userId; - uint64 tierLevel; - uint64 idx = users.nextElementIndex(NULL_INDEX); - uint32 numberOfProject; - Array votedList; - while (idx != NULL_INDEX) + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + const auto participants = nostromo.getAuctionParticipants(createOutput.auctionIndex, 0, NOST_AUCTION_GETTER_PAGE_SIZE); + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + uint64 allocatedTotal = 0; + for (uint64 participantIndex = 0; participantIndex < participants.returnedCount; ++participantIndex) { - userId = users.key(idx); - tierLevel = users.value(idx); + const auto participant = participants.participants.get(participantIndex); + EXPECT_LE(participant.allocatedQuantity, participant.requestedQuantity); + EXPECT_TRUE(participant.allocatedQuantity == 0 || participant.allocatedQuantity >= minimumPurchaseQuantity); + allocatedTotal += participant.allocatedQuantity; + } + EXPECT_EQ(allocatedTotal, auction.core.allocatedQuantity); + EXPECT_LE(allocatedTotal, quantityForSale); + EXPECT_EQ(nostromo.managedShares(asset, seller), static_cast(quantityForSale - allocatedTotal)); + } +} + +TEST(ContractNostromoAuction, BatchAvailabilityGetterStatesAuction) +{ + ContractTestingNOST nostromo; + const id batchSeller(645, 646, 647, 648); + const id standardSeller(649, 650, 651, 652); + const id bidder(653, 654, 655, 656); + const Asset closedBatchAsset{batchSeller, assetNameFromString("BGETCL")}; + const Asset maxBatchAsset{batchSeller, assetNameFromString("BGETMX")}; + const Asset standardAsset{standardSeller, assetNameFromString("BGETST")}; + + EXPECT_EQ(nostromo.getBatchAvailability(999).found, 0); + + EXPECT_EQ(nostromo.issueAsset(standardSeller, standardAsset.assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(standardSeller, standardAsset, 1), 1); + const auto standardCreate = + nostromo.createAuction(standardSeller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(standardAsset, 1))); + ASSERT_EQ(standardCreate.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getBatchAvailability(standardCreate.auctionIndex).found, 1); + EXPECT_EQ(nostromo.getBatchAvailability(standardCreate.auctionIndex).isAcceptingBids, 0); + + EXPECT_EQ(nostromo.issueAsset(batchSeller, closedBatchAsset.assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(batchSeller, closedBatchAsset, 1), 1); + const auto closedBatchCreate = nostromo.createAuction(batchSeller, ContractTestingNOST::makeBatchAuctionInput(closedBatchAsset, 1, 2)); + ASSERT_EQ(closedBatchCreate.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, closedBatchCreate.auctionIndex, 1, 2).errorCode, NOST::EAuctionError::Success); + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.getBatchAvailability(closedBatchCreate.auctionIndex).found, 1); + EXPECT_EQ(nostromo.getBatchAvailability(closedBatchCreate.auctionIndex).isAcceptingBids, 0); + + EXPECT_EQ(nostromo.issueAsset(batchSeller, maxBatchAsset.assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(batchSeller, maxBatchAsset, 1), 1); + const auto batchCreate = nostromo.createAuction(batchSeller, ContractTestingNOST::makeBatchAuctionInput(maxBatchAsset, 1, 2)); + ASSERT_EQ(batchCreate.errorCode, NOST::EAuctionError::Success); + NOST::AuctionParticipantData maxPriceBid{}; + maxPriceBid.auctionIndex = batchCreate.auctionIndex; + maxPriceBid.bidIndex = 0; + maxPriceBid.participant = bidder; + maxPriceBid.bidAmount = UINT64_MAX; + maxPriceBid.requestedQuantity = 1; + maxPriceBid.escrowedAmount = 1; + maxPriceBid.isUsed = 1; + maxPriceBid.isActive = 1; + maxPriceBid.isWinningBid = 1; + nostromo.stateData().participants.set(0, maxPriceBid); + EXPECT_EQ(nostromo.getBatchAvailability(batchCreate.auctionIndex).isAcceptingBids, 0); +} + +TEST(ContractNostromoAuction, PlaceStandardBidValidatesRefundsAndPauseAuction) +{ + ContractTestingNOST nostromo; + const id seller(121, 122, 123, 124); + const id bidderA(125, 126, 127, 128); + const id bidderB(129, 130, 131, 132); + const uint64 assetName = assetNameFromString("STDVAL"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = nostromo.createAuction( + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, + NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto sellerBid = nostromo.placeBid(seller, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE); + EXPECT_EQ(sellerBid.errorCode, NOST::EAuctionError::Forbidden); + + const auto lowStart = nostromo.placeBid(bidderA, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE - 1, NOST_STANDARD_MIN_PRICE - 1); + EXPECT_EQ(lowStart.errorCode, NOST::EAuctionError::BidTooLow); + + const auto openingBid = nostromo.placeBid(bidderA, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE); + ASSERT_EQ(openingBid.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(openingBid.escrowedAmount, NOST_STANDARD_MIN_PRICE); + + const auto lowIncrement = nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT - 1, + NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT - 1); + EXPECT_EQ(lowIncrement.errorCode, NOST::EAuctionError::BidTooLow); + + const auto outbid = nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT, + NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT); + ASSERT_EQ(outbid.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(outbid.refundedAmount, NOST_STANDARD_MIN_PRICE); + + const auto bidderAState = nostromo.getParticipant(createOutput.auctionIndex, bidderA); + const auto bidderBState = nostromo.getParticipant(createOutput.auctionIndex, bidderB); + ASSERT_EQ(bidderAState.found, 1); + ASSERT_EQ(bidderBState.found, 1); + EXPECT_EQ(bidderAState.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(bidderAState.participantData.isWinningBid, 0u); + EXPECT_EQ(bidderBState.participantData.escrowedAmount, NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT); + EXPECT_EQ(bidderBState.participantData.isWinningBid, 1u); + + const auto bidderBImprove = + nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 30000ULL, NOST_STANDARD_MIN_PRICE + 30000ULL); + EXPECT_EQ(bidderBImprove.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(bidderBImprove.refundedAmount, NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT); + EXPECT_EQ(bidderBImprove.escrowedAmount, NOST_STANDARD_MIN_PRICE + 30000ULL); + + nostromo.beginEpoch(); + EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + const auto pausedBid = nostromo.placeBid(id(133, 134, 135, 136), createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 40000ULL, + NOST_STANDARD_MIN_PRICE + 40000ULL); + EXPECT_EQ(pausedBid.errorCode, NOST::EAuctionError::AuctionPaused); + nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + + const auto resumedBid = nostromo.placeBid(id(137, 138, 139, 140), createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 40000ULL, + NOST_STANDARD_MIN_PRICE + 40000ULL); + EXPECT_EQ(resumedBid.errorCode, NOST::EAuctionError::Success); + + nostromo.setNow(2022, 4, 13, 12, 0, 0); + nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + + const auto bootstrapPausedBid = nostromo.placeBid(id(141, 142, 143, 144), createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 50000ULL, + NOST_STANDARD_MIN_PRICE + 50000ULL); + EXPECT_EQ(bootstrapPausedBid.errorCode, NOST::EAuctionError::AuctionPaused); +} + +TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(141, 142, 143, 144); + const id allowed(145, 146, 147, 148); + const id denied(149, 150, 151, 152); + const uint64 assetName = assetNameFromString("PRIBID"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 3, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowed}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + EXPECT_EQ(nostromo.placeBid(denied, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::PrivateAuctionAccessDenied); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(allowed, createOutput.auctionIndex, 1, 12).errorCode, NOST::EAuctionError::Success); + } + + { + ContractTestingNOST nostromo; + const id seller(153, 154, 155, 156); + const id gateIssuerA(157, 158, 159, 160); + const id gateIssuerB(161, 162, 163, 164); + const id belowThresholdBidder(165, 166, 167, 168); + const id exactThresholdBidder(169, 170, 171, 172); + const id alternateAssetBidder(173, 174, 175, 176); + const uint64 saleAssetName = assetNameFromString("PRIACS"); + const Asset saleAsset{seller, saleAssetName}; + const Asset accessAssetA{gateIssuerA, assetNameFromString("PRIAGA")}; + const Asset accessAssetB{gateIssuerB, assetNameFromString("PRIAGB")}; + + EXPECT_EQ(nostromo.issueAsset(seller, saleAssetName, 3), 3); + EXPECT_EQ(nostromo.issueAsset(gateIssuerA, accessAssetA.assetName, 5), 5); + EXPECT_EQ(nostromo.issueAsset(gateIssuerB, accessAssetB.assetName, 5), 5); + EXPECT_EQ(nostromo.transferAsset(gateIssuerA, belowThresholdBidder, accessAssetA, 2), 2); + EXPECT_EQ(nostromo.transferAsset(gateIssuerA, exactThresholdBidder, accessAssetA, 3), 3); + EXPECT_EQ(nostromo.transferAsset(gateIssuerB, alternateAssetBidder, accessAssetB, 5), 5); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, saleAsset, 3), 3); + + auto input = ContractTestingNOST::makeBatchAuctionInput(saleAsset, 3, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.requiredAccessAssets = + ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAssetA, 3}, NOST::AuctionAssetEntry{accessAssetB, 5}}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + EXPECT_EQ(nostromo.placeBid(belowThresholdBidder, createOutput.auctionIndex, 1, 12, 12).errorCode, + NOST::EAuctionError::PrivateAuctionAccessDenied); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(exactThresholdBidder, createOutput.auctionIndex, 1, 12).errorCode, + NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(alternateAssetBidder, createOutput.auctionIndex, 1, 13).errorCode, + NOST::EAuctionError::Success); + } +} + +TEST(ContractNostromoAuction, PrivateAuctionCombinedAccessUsesInclusiveOrAuction) +{ + ContractTestingNOST nostromo; + const id seller(177, 178, 179, 180); + const id gateIssuer(181, 182, 183, 184); + const id walletOnlyBidder(185, 186, 187, 188); + const id assetOnlyBidder(189, 190, 191, 192); + const id bothBidder(193, 194, 195, 196); + const id deniedBidder(197, 198, 199, 200); + const uint64 saleAssetName = assetNameFromString("PRIORA"); + const Asset saleAsset{seller, saleAssetName}; + const Asset accessAsset{gateIssuer, assetNameFromString("PRIORG")}; + + EXPECT_EQ(nostromo.issueAsset(seller, saleAssetName, 3), 3); + EXPECT_EQ(nostromo.issueAsset(gateIssuer, accessAsset.assetName, 2), 2); + EXPECT_EQ(nostromo.transferAsset(gateIssuer, assetOnlyBidder, accessAsset, 1), 1); + EXPECT_EQ(nostromo.transferAsset(gateIssuer, bothBidder, accessAsset, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, saleAsset, 3), 3); + + auto input = ContractTestingNOST::makeBatchAuctionInput(saleAsset, 3, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({walletOnlyBidder, bothBidder}); + input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAsset, 1}}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + const auto auctionOutput = nostromo.getAuction(createOutput.auctionIndex); + EXPECT_EQ(auctionOutput.auction.allowedBidderWalletCount, 2U); + EXPECT_EQ(auctionOutput.auction.requiredAccessAssetCount, 1U); + + nostromo.seedUser(deniedBidder, 100); + const sint64 deniedBalanceBefore = getBalance(deniedBidder); + const sint64 contractBalanceBeforeDeniedBid = getBalance(NOST_CONTRACT_ID); + EXPECT_EQ(nostromo.placeBidWithFundedReward(deniedBidder, createOutput.auctionIndex, 1, 12, 12).errorCode, + NOST::EAuctionError::PrivateAuctionAccessDenied); + EXPECT_EQ(getBalance(deniedBidder), deniedBalanceBefore); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBalanceBeforeDeniedBid); + EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, deniedBidder).found, 0); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(walletOnlyBidder, createOutput.auctionIndex, 1, 12).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(assetOnlyBidder, createOutput.auctionIndex, 1, 13).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(bothBidder, createOutput.auctionIndex, 1, 14).errorCode, NOST::EAuctionError::Success); +} + +TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) +{ + ContractTestingNOST nostromo; + const id seller(171, 172, 173, 174); + const id bidder(175, 176, 177, 178); + const uint64 assetName = assetNameFromString("BUYNWA"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); + + auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 3), NOST_STANDARD_MIN_PRICE, + NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT, + NOST_STANDARD_MIN_PRICE + 800000ULL); + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + const sint64 sellerBalanceBefore = getBalance(seller); + + const auto bidOutput = + nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 800000ULL, NOST_STANDARD_MIN_PRICE + 800000ULL); + ASSERT_EQ(bidOutput.errorCode, NOST::EAuctionError::Success); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + const auto participant = nostromo.getParticipant(createOutput.auctionIndex, bidder); + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.core.allocatedQuantity, 1ULL); + ASSERT_EQ(participant.found, 1); + EXPECT_EQ(participant.participantData.allocatedQuantity, 1ULL); + EXPECT_EQ(participant.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(participant.participantData.isWinningBid, 1u); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 3); + EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, 1683000ULL); +} + +TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialFillAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(181, 182, 183, 184); + const id bidderA(185, 186, 187, 188); + const id bidderB(189, 190, 191, 192); + const id bidderC(193, 194, 195, 196); + const uint64 assetName = assetNameFromString("BATFIN"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 4), 4); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 4), 4); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 4, 10)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 3, 15).errorCode, NOST::EAuctionError::Success); + nostromo.setNow(2026, 1, 1, 9, 0, 1); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 1, 15).errorCode, NOST::EAuctionError::Success); + nostromo.setNow(2026, 1, 1, 9, 0, 2); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderC, createOutput.auctionIndex, 2, 20).errorCode, NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + const auto participantA = nostromo.getParticipant(createOutput.auctionIndex, bidderA); + const auto participantB = nostromo.getParticipant(createOutput.auctionIndex, bidderB); + const auto participantC = nostromo.getParticipant(createOutput.auctionIndex, bidderC); + + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.core.allocatedQuantity, 4ULL); + ASSERT_EQ(participantA.found, 1); + ASSERT_EQ(participantB.found, 1); + ASSERT_EQ(participantC.found, 1); + EXPECT_EQ(participantC.participantData.allocatedQuantity, 2ULL); + EXPECT_EQ(participantA.participantData.allocatedQuantity, 2ULL); + EXPECT_EQ(participantB.participantData.allocatedQuantity, 0ULL); + EXPECT_EQ(participantA.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(participantB.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(participantC.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(participantA.participantData.isWinningBid, 1u); + EXPECT_EQ(participantB.participantData.isWinningBid, 0u); + EXPECT_EQ(participantC.participantData.isWinningBid, 1u); + EXPECT_EQ(nostromo.managedShares(asset, bidderA), 2); + EXPECT_EQ(nostromo.managedShares(asset, bidderB), 0); + EXPECT_EQ(nostromo.managedShares(asset, bidderC), 2); + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); + } + + { + ContractTestingNOST nostromo; + const id seller(197, 198, 199, 200); + const id bidder(201, 202, 203, 204); + const uint64 assetName = assetNameFromString("BATRET"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 5), 5); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 5), 5); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 5, 10)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, 2, 12).errorCode, NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.core.allocatedQuantity, 2ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 2); + EXPECT_EQ(nostromo.managedShares(asset, seller), 3); + } + + { + ContractTestingNOST nostromo; + const id seller(313, 314, 315, 316); + const id firstBidder(317, 318, 319, 320); + const id secondBidder(321, 322, 323, 324); + const Asset asset{seller, assetNameFromString("BATPRTL")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 15), 15); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 15), 15); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 15, 10); + input.minimumPurchaseQuantity = 10; + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + const sint64 sellerBalanceBefore = getBalance(seller); + nostromo.seedUser(firstBidder, 291); + nostromo.seedUser(secondBidder, 150); + const sint64 secondBidderBalanceBefore = getBalance(secondBidder); + + ASSERT_EQ(nostromo.placeBatchBidWithFundedRequiredReward(firstBidder, createOutput.auctionIndex, 10, 20).errorCode, + NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBidWithFundedReward(secondBidder, createOutput.auctionIndex, 10, 15, 150).errorCode, NOST::EAuctionError::BidTooLow); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto firstParticipant = nostromo.getParticipant(createOutput.auctionIndex, firstBidder); + const auto secondParticipant = nostromo.getParticipant(createOutput.auctionIndex, secondBidder); + ASSERT_EQ(firstParticipant.found, 1); + ASSERT_EQ(secondParticipant.found, 0); + EXPECT_EQ(firstParticipant.participantData.allocatedQuantity, 10ULL); + EXPECT_EQ(nostromo.managedShares(asset, firstBidder), 10); + EXPECT_EQ(nostromo.managedShares(asset, secondBidder), 0); + EXPECT_EQ(nostromo.managedShares(asset, seller), 5); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.allocatedQuantity, 10ULL); + EXPECT_EQ(getBalance(secondBidder), secondBidderBalanceBefore); + EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, 187ULL); + } + + { + ContractTestingNOST nostromo; + const id seller(325, 326, 327, 328); + const id firstBidder(329, 330, 331, 332); + const id partialBidder(333, 334, 335, 336); + const Asset asset{seller, assetNameFromString("BATPMIN")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 22), 22); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 22), 22); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 22, 10); + input.minimumPurchaseQuantity = 10; + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(firstBidder, createOutput.auctionIndex, 10, 22).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(partialBidder, createOutput.auctionIndex, 15, 22, 225).errorCode, NOST::EAuctionError::QuantityUnavailable); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto partialParticipant = nostromo.getParticipant(createOutput.auctionIndex, partialBidder); + ASSERT_EQ(partialParticipant.found, 0); + EXPECT_EQ(nostromo.managedShares(asset, partialBidder), 0); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.allocatedQuantity, 10ULL); + } +} + +TEST(ContractNostromoAuction, BatchFinalizationRefundsLosingBidsAndTieBreaksByBidTimeAuction) +{ + ContractTestingNOST nostromo; + const id seller(205, 206, 207, 208); + const id earlierBidder(209, 210, 211, 212); + const id laterBidder(213, 214, 215, 216); + const id higherBidder(217, 218, 219, 220); + const uint64 assetName = assetNameFromString("BATTIE"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 5)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + nostromo.seedUser(earlierBidder, 100); + nostromo.seedUser(laterBidder, 100); + nostromo.seedUser(higherBidder, 100); + const sint64 earlierBefore = getBalance(earlierBidder); + const sint64 laterBefore = getBalance(laterBidder); + const sint64 higherBefore = getBalance(higherBidder); + + ASSERT_EQ(nostromo.placeBatchBidWithFundedRequiredReward(earlierBidder, createOutput.auctionIndex, 1, 10).errorCode, + NOST::EAuctionError::Success); + nostromo.setNow(2026, 1, 1, 9, 0, 1); + ASSERT_EQ(nostromo.placeBatchBidWithFundedRequiredReward(laterBidder, createOutput.auctionIndex, 1, 10).errorCode, NOST::EAuctionError::Success); + nostromo.setNow(2026, 1, 1, 9, 0, 2); + ASSERT_EQ(nostromo.placeBatchBidWithFundedRequiredReward(higherBidder, createOutput.auctionIndex, 1, 11).errorCode, NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto earlier = nostromo.getParticipant(createOutput.auctionIndex, earlierBidder); + const auto later = nostromo.getParticipant(createOutput.auctionIndex, laterBidder); + const auto higher = nostromo.getParticipant(createOutput.auctionIndex, higherBidder); + ASSERT_EQ(earlier.found, 1); + ASSERT_EQ(later.found, 1); + ASSERT_EQ(higher.found, 1); + EXPECT_EQ(earlier.participantData.allocatedQuantity, 1ULL); + EXPECT_EQ(later.participantData.allocatedQuantity, 0ULL); + EXPECT_EQ(higher.participantData.allocatedQuantity, 1ULL); + EXPECT_EQ(earlier.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(later.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(higher.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(nostromo.managedShares(asset, earlierBidder), 1); + EXPECT_EQ(nostromo.managedShares(asset, laterBidder), 0); + EXPECT_EQ(nostromo.managedShares(asset, higherBidder), 1); + EXPECT_EQ(getBalance(earlierBidder), earlierBefore - 100); + EXPECT_EQ(getBalance(laterBidder), laterBefore - 90); + EXPECT_EQ(getBalance(higherBidder), higherBefore - 100); +} + +TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) +{ + ContractTestingNOST nostromo; + const id seller(211, 212, 213, 214); + const uint64 assetName = assetNameFromString("STDNOB"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(voteStatus.get(userId, votedList), 0); - EXPECT_EQ(numberOfVotedProject.get(userId, numberOfProject), 0); + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - idx = users.nextElementIndex(idx); + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.core.allocatedQuantity, 0ULL); + EXPECT_TRUE(isZero(auction.core.highestBidder)); + EXPECT_EQ(nostromo.managedShares(asset, seller), 1); +} + +TEST(ContractNostromoAuction, WeeklyPauseShiftsActiveAuctionDeadlineAuction) +{ + ContractTestingNOST nostromo; + const id seller(215, 216, 217, 218); + const uint64 assetName = assetNameFromString("PAUSHL"); + const Asset asset{seller, assetName}; + + nostromo.setNow(2026, 1, 6, 11, 40, 0); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + nostromo.setNow(2026, 1, 7, 11, 40, 0); + nostromo.advanceAndEndTick(0); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); + + nostromo.setNow(2026, 1, 7, 12, 0, 0); + nostromo.advanceAndEndTick(0); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); + + nostromo.setNow(2026, 1, 7, 12, 10, 1); + nostromo.advanceAndEndTick(0); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.managedShares(asset, seller), 1); +} + +TEST(ContractNostromoAuction, EndTickSkipsAuctionProcessingAtBootstrapTimeAuction) +{ + ContractTestingNOST nostromo; + const id seller(215, 216, 217, 218); + const uint64 assetName = assetNameFromString("BOOTTK"); + const Asset asset{seller, assetName}; + + nostromo.setNow(2022, 4, 12, 12, 0, 0); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + nostromo.setNow(2022, 4, 13, 12, 0, 0); + nostromo.advanceAndEndTick(0); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Active); + EXPECT_EQ(auction.core.allocatedQuantity, 0ULL); + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); +} + +TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) +{ + ContractTestingNOST nostromo; + const id seller(219, 220, 221, 222); + const id bidder(223, 224, 225, 226); + const uint64 assetName = assetNameFromString("PDSHFT"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = nostromo.createAuction( + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, + NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ( + nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 200000ULL, NOST_STANDARD_MIN_PRICE + 200000ULL).errorCode, + NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY) * 1000ULL); + auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + const auto originalSellerDecisionDeadline = auction.core.sellerDecisionDeadline; + ASSERT_EQ(auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(auction.core.sellerDecisionDeadline.getHour(), 9); + EXPECT_EQ(auction.core.sellerDecisionDeadline.getMinute(), 0); + EXPECT_EQ(auction.core.sellerDecisionDeadline.getSecond(), 0); + + nostromo.setNow(2026, 1, 9, 8, 59, 50); + nostromo.beginEpoch(); + const uint32 launchPauseTicksAfterBeginEpoch = nostromo.getTicksBeforeAuctionLaunch().ticks; + EXPECT_EQ(launchPauseTicksAfterBeginEpoch, NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + + nostromo.advanceAndEndTick(1000); + EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, launchPauseTicksAfterBeginEpoch - 1); + + nostromo.setNow(2026, 1, 9, 9, 8, 10); + nostromo.advanceAndEndTick(0); + auction = nostromo.getAuction(createOutput.auctionIndex).auction; + ASSERT_EQ(auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(auction.core.sellerDecisionDeadline, originalSellerDecisionDeadline); + + nostromo.advanceTicks(launchPauseTicksAfterBeginEpoch - 2); + auction = nostromo.getAuction(createOutput.auctionIndex).auction; + ASSERT_EQ(auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, 0U); + EXPECT_GT(auction.core.sellerDecisionDeadline, originalSellerDecisionDeadline); + + auto shiftedDeadline = auction.core.sellerDecisionDeadline; + shiftedDeadline.add(0, 0, 0, 0, 0, -1); + nostromo.setNow(shiftedDeadline.getYear(), shiftedDeadline.getMonth(), shiftedDeadline.getDay(), shiftedDeadline.getHour(), + shiftedDeadline.getMinute(), shiftedDeadline.getSecond()); + nostromo.advanceAndEndTick(0); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); + + shiftedDeadline = auction.core.sellerDecisionDeadline; + shiftedDeadline.add(0, 0, 0, 0, 0, 1); + nostromo.setNow(shiftedDeadline.getYear(), shiftedDeadline.getMonth(), shiftedDeadline.getDay(), shiftedDeadline.getHour(), + shiftedDeadline.getMinute(), shiftedDeadline.getSecond()); + nostromo.advanceAndEndTick(0); + auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); +} + +TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) +{ + const uint8 routeModes[] = {0, 1}; + for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) + { + SCOPED_TRACE(::testing::Message() << "routeAllFeesToDevelopment=" << static_cast(routeModes[routeIndex])); + ContractTestingNOST nostromo; + const uint8 routeMode = routeModes[routeIndex]; + const id seller(221 + routeIndex, 222 + routeIndex, 223 + routeIndex, 224 + routeIndex); + const id bidder(225 + routeIndex, 226 + routeIndex, 227 + routeIndex, 228 + routeIndex); + const uint64 assetName = assetNameFromString(routeMode ? "STDENR1" : "STDENR0"); + const Asset asset{seller, assetName}; + + nostromo.setRouteAllFeesToDevelopment(routeMode); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + // Isolate the sale-fee pool from the auction creation fee. + nostromo.endEpoch(); + constexpr uint64 expectedSellerPayout = 935000ULL; + constexpr uint64 expectedShareholderDividend = 45000ULL; + constexpr uint64 expectedManagementFee = 5000ULL; + constexpr uint64 expectedDevelopmentFee = 5000ULL; + constexpr uint64 expectedCoordinatorFee = 10000ULL; + constexpr uint64 expectedTotalFees = 65000ULL; + const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedShareholderDividend); + const sint64 sellerBalanceBefore = getBalance(seller); + const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, + NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.core.allocatedQuantity, 1ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); + EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, expectedSellerPayout); + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedTotalFees); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, expectedTotalFees); + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 0ULL); + + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedTotalFees); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); } - } - void getStatsChecker(uint64 epochRevenu_t, uint64 totalPoolWeight_t, uint32 numberOfCreatedProject_t, uint32 numberOfFundraising_t, uint32 numberOfRegister_t) - { - EXPECT_EQ(epochRevenu_t, epochRevenue); - EXPECT_EQ(totalPoolWeight_t, totalPoolWeight); - EXPECT_EQ(numberOfCreatedProject_t, numberOfCreatedProject); - EXPECT_EQ(numberOfFundraising_t, numberOfFundraising); - EXPECT_EQ(numberOfRegister_t, numberOfRegister); - } - void removeElementAfterClaimChecker(id user) - { - uint32 tp; - EXPECT_EQ(investors.get(user, tmpInvestedList), 0); - EXPECT_EQ(numberOfInvestedProjects.get(user, tp), 0); - } -}; + else + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedManagementFee); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedDevelopmentFee); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedCoordinatorFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); + } + } +} -class ContractTestingNostromo : protected ContractTesting +TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction) { -public: - ContractTestingNostromo() - { - initEmptySpectrum(); - initEmptyUniverse(); - INIT_CONTRACT(NOST); - callSystemProcedure(NOST_CONTRACT_INDEX, INITIALIZE); - INIT_CONTRACT(QX); - callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); - INIT_CONTRACT(QUOTTERY); - callSystemProcedure(QUOTTERY_CONTRACT_INDEX, INITIALIZE); - } - NostromoChecker* getState() - { - return (NostromoChecker*)contractStates[NOST_CONTRACT_INDEX]; - } - void endEpoch(bool expectSuccess = true) - { - callSystemProcedure(NOST_CONTRACT_INDEX, END_EPOCH, expectSuccess); - } - void registerInTier(const id& registerId, - uint32 tierLevel, - uint64 depositeAmount) - { - NOST::registerInTier_input input; - NOST::registerInTier_output output; - - input.tierLevel = tierLevel; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 1, input, output, registerId, depositeAmount); - } - void logoutFromTier(const id& registerId) - { - NOST::logoutFromTier_input input; - NOST::logoutFromTier_output output; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 2, input, output, registerId, 0); - } - void createProject(const id& registerId, - uint64 tokenName, - uint64 supply, - uint32 startYear, - uint32 startMonth, - uint32 startDay, - uint32 startHour, - uint32 endYear, - uint32 endMonth, - uint32 endDay, - uint32 endHour) - { - NOST::createProject_input input; - NOST::createProject_output output; - - input.tokenName = tokenName; - input.supply = supply; - input.startYear = startYear; - input.startMonth = startMonth; - input.startDay = startDay; - input.startHour = startHour; - input.endYear = endYear; - input.endMonth = endMonth; - input.endDay = endDay; - input.endHour = endHour; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 3, input, output, registerId, NOSTROMO_CREATE_PROJECT_FEE); - } - void voteInProject(const id& registerId, - uint32 indexOfProject, - bit decision) - { - NOST::voteInProject_input input; - NOST::voteInProject_output output; - - input.decision = decision; - input.indexOfProject = indexOfProject; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 4, input, output, registerId, 0); - } - void createFundraising(const id& registerId, - uint64 tokenPrice, - uint64 soldAmount, - uint64 requiredFunds, - - uint32 indexOfProject, - uint32 firstPhaseStartYear, - uint32 firstPhaseStartMonth, - uint32 firstPhaseStartDay, - uint32 firstPhaseStartHour, - uint32 firstPhaseEndYear, - uint32 firstPhaseEndMonth, - uint32 firstPhaseEndDay, - uint32 firstPhaseEndHour, - - uint32 secondPhaseStartYear, - uint32 secondPhaseStartMonth, - uint32 secondPhaseStartDay, - uint32 secondPhaseStartHour, - uint32 secondPhaseEndYear, - uint32 secondPhaseEndMonth, - uint32 secondPhaseEndDay, - uint32 secondPhaseEndHour, - - uint32 thirdPhaseStartYear, - uint32 thirdPhaseStartMonth, - uint32 thirdPhaseStartDay, - uint32 thirdPhaseStartHour, - uint32 thirdPhaseEndYear, - uint32 thirdPhaseEndMonth, - uint32 thirdPhaseEndDay, - uint32 thirdPhaseEndHour, - - uint32 listingStartYear, - uint32 listingStartMonth, - uint32 listingStartDay, - uint32 listingStartHour, - - uint32 cliffEndYear, - uint32 cliffEndMonth, - uint32 cliffEndDay, - uint32 cliffEndHour, - - uint32 vestingEndYear, - uint32 vestingEndMonth, - uint32 vestingEndDay, - uint32 vestingEndHour, - - uint8 threshold, - uint8 TGE, - uint8 stepOfVesting) - { - NOST::createFundraising_input input; - NOST::createFundraising_output output; - - input.tokenPrice = tokenPrice; - input.soldAmount = soldAmount; - input.requiredFunds = requiredFunds; - - input.indexOfProject = indexOfProject; - input.firstPhaseStartYear = firstPhaseStartYear; - input.firstPhaseStartMonth = firstPhaseStartMonth; - input.firstPhaseStartDay = firstPhaseStartDay; - input.firstPhaseStartHour = firstPhaseStartHour; - input.firstPhaseEndYear = firstPhaseEndYear; - input.firstPhaseEndMonth = firstPhaseEndMonth; - input.firstPhaseEndDay = firstPhaseEndDay; - input.firstPhaseEndHour = firstPhaseEndHour; - - input.secondPhaseStartYear = secondPhaseStartYear; - input.secondPhaseStartMonth = secondPhaseStartMonth; - input.secondPhaseStartDay = secondPhaseStartDay; - input.secondPhaseStartHour = secondPhaseStartHour; - input.secondPhaseEndYear = secondPhaseEndYear; - input.secondPhaseEndMonth = secondPhaseEndMonth; - input.secondPhaseEndDay = secondPhaseEndDay; - input.secondPhaseEndHour = secondPhaseEndHour; - - input.thirdPhaseStartYear = thirdPhaseStartYear; - input.thirdPhaseStartMonth = thirdPhaseStartMonth; - input.thirdPhaseStartDay = thirdPhaseStartDay; - input.thirdPhaseStartHour = thirdPhaseStartHour; - input.thirdPhaseEndYear = thirdPhaseEndYear; - input.thirdPhaseEndMonth = thirdPhaseEndMonth; - input.thirdPhaseEndDay = thirdPhaseEndDay; - input.thirdPhaseEndHour = thirdPhaseEndHour; - - input.listingStartYear = listingStartYear; - input.listingStartMonth = listingStartMonth; - input.listingStartDay = listingStartDay; - input.listingStartHour = listingStartHour; - - input.cliffEndYear = cliffEndYear; - input.cliffEndMonth = cliffEndMonth; - input.cliffEndDay = cliffEndDay; - input.cliffEndHour = cliffEndHour; - - input.vestingEndYear = vestingEndYear; - input.vestingEndMonth = vestingEndMonth; - input.vestingEndDay = vestingEndDay; - input.vestingEndHour = vestingEndHour; - - input.threshold = threshold; - input.TGE = TGE; - input.stepOfVesting = stepOfVesting; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 5, input, output, registerId, NOSTROMO_QX_TOKEN_ISSUANCE_FEE); - } - void investInProject(const id& investorId, - uint32 indexOfFundraising, - uint64 investmentAmount) - { - NOST::investInProject_input input; - NOST::investInProject_output output; - - input.indexOfFundraising = indexOfFundraising; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 6, input, output, investorId, investmentAmount); - } - uint64 claimToken(const id& claimerId, - uint64 claimAmount, - uint32 indexOfFundraising) - { - NOST::claimToken_input input; - NOST::claimToken_output output; - - input.amount = claimAmount; - input.indexOfFundraising = indexOfFundraising; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 7, input, output, claimerId, 0); - return output.claimedAmount; - } - void upgradeTier(const id& registerId, - uint32 newTierLevel, - uint64 depositAmount) - { - NOST::upgradeTier_input input; - NOST::upgradeTier_output output; - - input.newTierLevel = newTierLevel; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 8, input, output, registerId, depositAmount); - } - sint64 TransferShareManagementRights(const id& user, Asset asset, sint64 numberOfShares, uint32 newManagingContractIndex) - { - NOST::TransferShareManagementRights_input input; - NOST::TransferShareManagementRights_output output; - - input.asset = asset; - input.newManagingContractIndex = newManagingContractIndex; - input.numberOfShares = numberOfShares; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 9, input, output, user, 100); - - return output.transferredNumberOfShares; - } - NOST::getStats_output getStats() const - { - NOST::getStats_input input; - NOST::getStats_output output; - - callFunction(NOST_CONTRACT_INDEX, 1, input, output); - return output; - } - NOST::getTierLevelByUser_output getTierLevelByUser(const id& registerId) const - { - NOST::getTierLevelByUser_input input; - NOST::getTierLevelByUser_output output; - - input.userId = registerId; - callFunction(NOST_CONTRACT_INDEX, 2, input, output); - return output; - } - NOST::getUserVoteStatus_output getUserVoteStatus(const id& registerId) const - { - NOST::getUserVoteStatus_input input; - NOST::getUserVoteStatus_output output; - - input.userId = registerId; - callFunction(NOST_CONTRACT_INDEX, 3, input, output); - return output; - } - NOST::checkTokenCreatability_output checkTokenCreatability(uint64 tokenName) const - { - NOST::checkTokenCreatability_input input; - NOST::checkTokenCreatability_output output; - - input.tokenName = tokenName; - callFunction(NOST_CONTRACT_INDEX, 4, input, output); - return output; - } - NOST::getNumberOfInvestedProjects_output getNumberOfInvestedProjects(const id& invsetorId) const - { - NOST::getNumberOfInvestedProjects_input input; - NOST::getNumberOfInvestedProjects_output output; - - input.userId = invsetorId; - callFunction(NOST_CONTRACT_INDEX, 5, input, output); - return output; - } - NOST::getProjectByIndex_output getProjectByIndex(uint32 indexOfProject) const - { - NOST::getProjectByIndex_input input; - NOST::getProjectByIndex_output output; - - input.indexOfProject = indexOfProject; - callFunction(NOST_CONTRACT_INDEX, 6, input, output); - return output; - } - NOST::getFundarasingByIndex_output getFundarasingByIndex(uint32 indexOfFundraising) const - { - NOST::getFundarasingByIndex_input input; - NOST::getFundarasingByIndex_output output; - - input.indexOfFundarasing = indexOfFundraising; - callFunction(NOST_CONTRACT_INDEX, 7, input, output); - return output; - } - NOST::getProjectIndexListByCreator_output getProjectIndexListByCreator(const id& creatorId) const - { - NOST::getProjectIndexListByCreator_input input; - NOST::getProjectIndexListByCreator_output output; - - input.creator = creatorId; - callFunction(NOST_CONTRACT_INDEX, 8, input, output); - return output; - } - NOST::getInfoUserInvested_output getInfoUserInvested(const id& investorId) const - { - NOST::getInfoUserInvested_input input; - NOST::getInfoUserInvested_output output; - - input.investorId = investorId; - callFunction(NOST_CONTRACT_INDEX, 9, input, output); - return output; - } - uint64 getMaxClaimAmount(const id& investorId, uint32 indexOfFundraising) const - { - NOST::getMaxClaimAmount_input input; - NOST::getMaxClaimAmount_output output; - - input.investorId = investorId; - input.indexOfFundraising = indexOfFundraising; - callFunction(NOST_CONTRACT_INDEX, 10, input, output); - return output.amount; - } -}; + { + ContractTestingNOST nostromo; + const id seller(231, 232, 233, 234); + const id bidder(235, 236, 237, 238); + const uint64 assetName = assetNameFromString("PENACC"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = nostromo.createAuction( + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, + NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 200000ULL, NOST_STANDARD_MIN_PRICE + 200000ULL) + .errorCode, + NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); + + const auto forbidden = nostromo.resolvePendingStandardAuction(id(999, 999, 999, 999), createOutput.auctionIndex, true); + EXPECT_EQ(forbidden.errorCode, NOST::EAuctionError::Forbidden); + + const auto acceptOutput = nostromo.resolvePendingStandardAuction(seller, createOutput.auctionIndex, true); + EXPECT_EQ(acceptOutput.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); + } + + { + ContractTestingNOST nostromo; + const id seller(239, 240, 241, 242); + const id bidder(243, 244, 245, 246); + const uint64 assetName = assetNameFromString("PENREJ"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = nostromo.createAuction( + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, + NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + nostromo.seedUser(bidder, NOST_STANDARD_MIN_PRICE + 300000ULL); + const sint64 bidderBeforeBid = getBalance(bidder); + ASSERT_EQ(nostromo + .placeBidWithFundedReward(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 200000ULL, + NOST_STANDARD_MIN_PRICE + 200000ULL) + .errorCode, + NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + const auto rejectOutput = nostromo.resolvePendingStandardAuction(seller, createOutput.auctionIndex, false); + EXPECT_EQ(rejectOutput.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(rejectOutput.refundedAmount, NOST_STANDARD_MIN_PRICE + 200000ULL); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + const auto participant = nostromo.getParticipant(createOutput.auctionIndex, bidder); + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.core.allocatedQuantity, 0ULL); + EXPECT_TRUE(isZero(auction.core.highestBidder)); + ASSERT_EQ(participant.found, 1); + EXPECT_EQ(participant.participantData.allocatedQuantity, 0ULL); + EXPECT_EQ(participant.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(nostromo.managedShares(asset, seller), 1); + EXPECT_EQ(getBalance(bidder), bidderBeforeBid); + } + + { + ContractTestingNOST nostromo; + const id seller(247, 248, 249, 250); + const id bidder(251, 252, 253, 254); + const uint64 assetName = assetNameFromString("PENTMO"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = nostromo.createAuction( + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, + NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 200000ULL, NOST_STANDARD_MIN_PRICE + 200000ULL) + .errorCode, + NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); + + nostromo.advanceAndEndTick((NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS + 1ULL) * 1000ULL); + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + const auto participant = nostromo.getParticipant(createOutput.auctionIndex, bidder); + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.core.allocatedQuantity, 1ULL); + ASSERT_EQ(participant.found, 1); + EXPECT_EQ(participant.participantData.allocatedQuantity, 1ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); + } +} + +TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) +{ + const uint8 routeModes[] = {0, 1}; + for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) + { + SCOPED_TRACE(::testing::Message() << "routeAllFeesToDevelopment=" << static_cast(routeModes[routeIndex])); + { + ContractTestingNOST nostromo; + const uint8 routeMode = routeModes[routeIndex]; + const id seller(261 + routeIndex, 262 + routeIndex, 263 + routeIndex, 264 + routeIndex); + const uint64 assetName = assetNameFromString(routeMode ? "CANBT1" : "CANBT0"); + const Asset asset{seller, assetName}; + + nostromo.setRouteAllFeesToDevelopment(routeMode); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 10), 10); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 1000)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + nostromo.endEpoch(); + const sint64 sellerBefore = getBalance(seller); + const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + constexpr uint64 expectedShareholderDividend = 727ULL; + constexpr uint64 expectedRecipientFee = 91ULL; + const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedShareholderDividend); + + const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionIndex, 1000); + EXPECT_EQ(cancelOutput.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(cancelOutput.refundedAmount, 0ULL); + EXPECT_EQ(cancelOutput.cancellationFee, 1000ULL); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Cancelled); + EXPECT_EQ(nostromo.managedShares(asset, seller), 10); + EXPECT_EQ(getBalance(seller) - sellerBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 1000ULL); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 1000ULL); + nostromo.endEpoch(); + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 1000ULL); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); + } + else + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedRecipientFee); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedRecipientFee); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedRecipientFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); + } + } + + { + ContractTestingNOST nostromo; + const uint8 routeMode = routeModes[routeIndex]; + const id seller(273 + routeIndex, 274 + routeIndex, 275 + routeIndex, 276 + routeIndex); + const uint64 assetName = assetNameFromString(routeMode ? "CANST1" : "CANST0"); + const Asset asset{seller, assetName}; + + nostromo.setRouteAllFeesToDevelopment(routeMode); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + nostromo.endEpoch(); + const sint64 sellerBefore = getBalance(seller); + const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + constexpr uint64 expectedShareholderDividend = 72700ULL; + constexpr uint64 expectedRecipientFee = 9100ULL; + const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedShareholderDividend); + + const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionIndex, 100000); + EXPECT_EQ(cancelOutput.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(cancelOutput.refundedAmount, 0ULL); + EXPECT_EQ(cancelOutput.cancellationFee, 100000ULL); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Cancelled); + EXPECT_EQ(nostromo.managedShares(asset, seller), 1); + EXPECT_EQ(getBalance(seller) - sellerBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 100000ULL); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 100000ULL); + nostromo.endEpoch(); + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 100000ULL); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); + } + else + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedRecipientFee); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedRecipientFee); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedRecipientFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); + } + } + } +} + +TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeRemainderAuction) +{ + const uint8 routeModes[] = {0, 1}; + for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) + { + SCOPED_TRACE(::testing::Message() << "routeAllFeesToDevelopment=" << static_cast(routeModes[routeIndex])); + { + ContractTestingNOST nostromo; + const uint8 routeMode = routeModes[routeIndex]; + const id batchSeller(277 + routeIndex, 278 + routeIndex, 279 + routeIndex, 280 + routeIndex); + const uint64 batchAssetName = assetNameFromString(routeMode ? "CANRN1" : "CANRN0"); + const Asset batchAsset{batchSeller, batchAssetName}; + + nostromo.setRouteAllFeesToDevelopment(routeMode); + EXPECT_EQ(nostromo.issueAsset(batchSeller, batchAssetName, 7), 7); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(batchSeller, batchAsset, 7), 7); + + const auto batchCreateOutput = nostromo.createAuction(batchSeller, ContractTestingNOST::makeBatchAuctionInput(batchAsset, 7, 333)); + ASSERT_EQ(batchCreateOutput.errorCode, NOST::EAuctionError::Success); + nostromo.endEpoch(); + + constexpr uint64 expectedBatchShareholderDividend = 170ULL; + constexpr uint64 expectedBatchRecipientFee = 21ULL; + const sint64 expectedBatchDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedBatchShareholderDividend); + const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + + const auto batchCancelOutput = nostromo.cancelAuction(batchSeller, batchCreateOutput.auctionIndex, 233); + EXPECT_EQ(batchCancelOutput.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(batchCancelOutput.cancellationFee, 233ULL); + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 233ULL); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 233ULL); + nostromo.endEpoch(); + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 233ULL); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); + } + else + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedBatchRecipientFee); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedBatchRecipientFee); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedBatchRecipientFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedBatchDividendPoolIncrease); + } + EXPECT_EQ(expectedBatchShareholderDividend + expectedBatchRecipientFee * 3ULL, batchCancelOutput.cancellationFee); + } + + { + ContractTestingNOST smallFeeNostromo; + const uint8 routeMode = routeModes[routeIndex]; + const id standardSeller(281 + routeIndex, 282 + routeIndex, 283 + routeIndex, 284 + routeIndex); + const uint64 standardAssetName = assetNameFromString(routeMode ? "CANON1" : "CANON0"); + const Asset standardAsset{standardSeller, standardAssetName}; + + smallFeeNostromo.setRouteAllFeesToDevelopment(routeMode); + EXPECT_EQ(smallFeeNostromo.issueAsset(standardSeller, standardAssetName, 1), 1); + EXPECT_EQ(smallFeeNostromo.transferShareManagementRightsToNostromo(standardSeller, standardAsset, 1), 1); + + const auto standardCreateOutput = + smallFeeNostromo.createAuction(standardSeller, ContractTestingNOST::makeBatchAuctionInput(standardAsset, 1, 19)); + ASSERT_EQ(standardCreateOutput.errorCode, NOST::EAuctionError::Success); + smallFeeNostromo.endEpoch(); + + const sint64 expectedSmallDividendPoolIncrease = smallFeeNostromo.expectedDividendPoolIncrease(1ULL); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + + const auto standardCancelOutput = smallFeeNostromo.cancelAuction(standardSeller, standardCreateOutput.auctionIndex, 1); + EXPECT_EQ(standardCancelOutput.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(standardCancelOutput.cancellationFee, 1ULL); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 1ULL); + EXPECT_EQ(smallFeeNostromo.getNostromoFeePool().totalAmount, 1ULL); + smallFeeNostromo.endEpoch(); + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 1ULL); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); + } + else + { + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedSmallDividendPoolIncrease); + } + } + } +} + +TEST(ContractNostromoAuction, CancelAuctionRejectsAfterAcceptedBidWithoutSideEffectsAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(281, 282, 283, 284); + const id bidder(285, 286, 287, 288); + const uint64 assetName = assetNameFromString("CANINV"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, 1, 12).errorCode, NOST::EAuctionError::Success); + + const auto notFound = nostromo.cancelAuction(seller, 800, 10); + EXPECT_EQ(notFound.errorCode, NOST::EAuctionError::AuctionNotFound); + + const auto forbidden = nostromo.cancelAuction(bidder, createOutput.auctionIndex, 10); + EXPECT_EQ(forbidden.errorCode, NOST::EAuctionError::Forbidden); + + const auto bidderBalanceBeforeCancel = getBalance(bidder); + const auto participantBeforeCancel = nostromo.getParticipant(createOutput.auctionIndex, bidder); + const auto rejected = nostromo.cancelAuction(seller, createOutput.auctionIndex, 0); + EXPECT_EQ(rejected.errorCode, NOST::EAuctionError::AuctionHasAcceptedBid); + EXPECT_EQ(rejected.refundedAmount, 0ULL); + EXPECT_EQ(getBalance(bidder), bidderBalanceBeforeCancel); + const auto participantAfterCancel = nostromo.getParticipant(createOutput.auctionIndex, bidder); + ASSERT_EQ(participantBeforeCancel.found, 1); + ASSERT_EQ(participantAfterCancel.found, 1); + EXPECT_EQ(participantAfterCancel.participantData.escrowedAmount, participantBeforeCancel.participantData.escrowedAmount); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); + EXPECT_EQ(nostromo.managedShares(asset, NOST_CONTRACT_ID), 2); + } + + { + ContractTestingNOST standardNostromo; + const id standardSeller(1281, 1282, 1283, 1284); + const id standardBidder(1285, 1286, 1287, 1288); + const Asset standardAsset{standardSeller, assetNameFromString("CANSTD")}; + ASSERT_EQ(standardNostromo.issueAsset(standardSeller, standardAsset.assetName, 1), 1); + ASSERT_EQ(standardNostromo.transferShareManagementRightsToNostromo(standardSeller, standardAsset, 1), 1); + const auto standardCreate = standardNostromo.createAuction( + standardSeller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(standardAsset, 1))); + ASSERT_EQ(standardCreate.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ( + standardNostromo.placeBid(standardBidder, standardCreate.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, + NOST::EAuctionError::Success); + EXPECT_EQ(standardNostromo.cancelAuction(standardSeller, standardCreate.auctionIndex, 0).errorCode, + NOST::EAuctionError::AuctionHasAcceptedBid); + EXPECT_EQ(standardNostromo.getAuction(standardCreate.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); + } +} + +TEST(ContractNostromoAuction, CancelAuctionRejectsInvalidCasesWithoutBidsAuction) +{ + ContractTestingNOST nostromo; + const id seller(289, 290, 291, 292); + const id outsider(293, 294, 295, 296); + const uint64 assetName = assetNameFromString("CANIN2"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto notFound = nostromo.cancelAuction(seller, 801, 10); + EXPECT_EQ(notFound.errorCode, NOST::EAuctionError::AuctionNotFound); + + const auto forbidden = nostromo.cancelAuction(outsider, createOutput.auctionIndex, 10); + EXPECT_EQ(forbidden.errorCode, NOST::EAuctionError::Forbidden); + + const auto insufficient = nostromo.cancelAuction(seller, createOutput.auctionIndex, 1); + EXPECT_EQ(insufficient.errorCode, NOST::EAuctionError::InsufficientFunds); + + const auto success = nostromo.cancelAuction(seller, createOutput.auctionIndex, 2); + EXPECT_EQ(success.errorCode, NOST::EAuctionError::Success); + + const auto closed = nostromo.cancelAuction(seller, createOutput.auctionIndex, 2); + EXPECT_EQ(closed.errorCode, NOST::EAuctionError::AuctionClosed); +} + +TEST(ContractNostromoAuction, FinalizationArchivesRecordsAndReusesActiveSlotsAuction) +{ + ContractTestingNOST nostromo; + const id seller(481, 482, 483, 484); + const id bidderA(485, 486, 487, 488); + const id bidderB(489, 490, 491, 492); + const Asset asset{seller, assetNameFromString("REUSEA")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + auto firstInput = + ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE, + NOST_STANDARD_MIN_BID_INCREMENT, NOST_STANDARD_MIN_PRICE); + const auto firstAuction = nostromo.createAuction(seller, firstInput); + ASSERT_EQ(firstAuction.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidderA, firstAuction.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, + NOST::EAuctionError::Success); + + EXPECT_EQ(nostromo.stateData().auctionList.population(), 0ULL); + EXPECT_EQ(nostromo.stateData().participantHistoryCounter, 1ULL); + EXPECT_EQ(nostromo.getAuction(firstAuction.auctionIndex).auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.getParticipant(firstAuction.auctionIndex, bidderA).found, 1); + + const auto secondAuction = nostromo.createAuction(seller, firstInput); + ASSERT_EQ(secondAuction.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidderB, secondAuction.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, + NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.stateData().participantHistoryCounter, 2ULL); + EXPECT_EQ(nostromo.getAuction(secondAuction.auctionIndex).auction.core.status, NOST::EAuctionStatus::Finalized); +} + +TEST(ContractNostromoAuction, ClosedAuctionHistoryRecordsFinalizedAndCancelledAuctionsAuction) +{ + ContractTestingNOST nostromo; + const id finalizedSeller(501, 502, 503, 504); + const id bidder(505, 506, 507, 508); + const uint64 finalizedAssetName = assetNameFromString("HISFIN"); + const Asset finalizedAsset{finalizedSeller, finalizedAssetName}; + const id cancelledSeller(509, 510, 511, 512); + const uint64 cancelledAssetName = assetNameFromString("HISCAN"); + const Asset cancelledAsset{cancelledSeller, cancelledAssetName}; + + EXPECT_EQ(nostromo.issueAsset(finalizedSeller, finalizedAssetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(finalizedSeller, finalizedAsset, 1), 1); + const auto finalizedCreateOutput = nostromo.createAuction(finalizedSeller, ContractTestingNOST::makeBatchAuctionInput(finalizedAsset, 1, 10)); + ASSERT_EQ(finalizedCreateOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, finalizedCreateOutput.auctionIndex, 1, 10).errorCode, NOST::EAuctionError::Success); + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + EXPECT_EQ(nostromo.issueAsset(cancelledSeller, cancelledAssetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(cancelledSeller, cancelledAsset, 1), 1); + const auto cancelledCreateOutput = nostromo.createAuction(cancelledSeller, ContractTestingNOST::makeBatchAuctionInput(cancelledAsset, 1, 10)); + ASSERT_EQ(cancelledCreateOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.cancelAuction(cancelledSeller, cancelledCreateOutput.auctionIndex, 1).errorCode, NOST::EAuctionError::Success); + + const auto history = nostromo.getClosedAuctionHistory(); + EXPECT_EQ(history.totalEntries, 2ULL); + EXPECT_TRUE(containsAuctionIndex(history.auctionIndices, history.totalEntries, finalizedCreateOutput.auctionIndex)); + EXPECT_TRUE(containsAuctionIndex(history.auctionIndices, history.totalEntries, cancelledCreateOutput.auctionIndex)); + EXPECT_EQ(nostromo.getAuction(finalizedCreateOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.getAuction(cancelledCreateOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Cancelled); +} + +TEST(ContractNostromoAuction, ClosedAuctionHistoryGetterExposesRingBufferOverwriteAuction) +{ + ContractTestingNOST nostromo; + const uint64 overwrittenAuctionIndex = 22000; + const uint64 latestAuctionIndex = 23000; + NOST::AuctionData archivedAuction{}; + + archivedAuction.core.auctionIndex = overwrittenAuctionIndex; + archivedAuction.core.status = NOST::EAuctionStatus::Finalized; + nostromo.stateData().closedAuctionHistory.set(0, archivedAuction); + nostromo.stateData().closedAuctionHistoryCounter = 1; + for (uint64 index = 1; index < NOST_AUCTION_HISTORY_NUM; ++index) + { + archivedAuction.core.auctionIndex = 24000 + index; + nostromo.stateData().closedAuctionHistory.set(index, archivedAuction); + ++nostromo.stateData().closedAuctionHistoryCounter; + } + archivedAuction.core.auctionIndex = latestAuctionIndex; + nostromo.stateData().closedAuctionHistory.set(0, archivedAuction); + ++nostromo.stateData().closedAuctionHistoryCounter; + + const auto history = nostromo.getClosedAuctionHistory(); + EXPECT_EQ(history.totalEntries, NOST_AUCTION_HISTORY_NUM + 1ULL); + EXPECT_FALSE(containsAuctionIndex(history.auctionIndices, history.totalEntries, overwrittenAuctionIndex)); + EXPECT_TRUE(containsAuctionIndex(history.auctionIndices, history.totalEntries, latestAuctionIndex)); + EXPECT_EQ(history.auctionIndices.get(0), latestAuctionIndex); + EXPECT_EQ(nostromo.getAuction(overwrittenAuctionIndex).found, 0); + EXPECT_EQ(nostromo.getAuction(latestAuctionIndex).found, 1); +} + +TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) +{ + ContractTestingNOST nostromo; + const id outsider(291, 292, 293, 294); + const id newManagement(295, 296, 297, 298); + + NOST::SetAuctionFees_input coordinatorInput{}; + coordinatorInput.privateAuctionFee = 60000000; + coordinatorInput.publicAuctionCreationFee = 123; + coordinatorInput.auctionCancellationFeeBasisPoints = 900; + coordinatorInput.managementFeeBasisPoints = 60; + coordinatorInput.developmentFeeBasisPoints = 70; + coordinatorInput.takeoverCoordinatorFeeBasisPoints = 80; + coordinatorInput.shareholderDividendBasisPoints = 8500; + coordinatorInput.shareholderFeeBasisPointsTier1 = 400; + coordinatorInput.shareholderFeeBasisPointsTier2 = 350; + coordinatorInput.shareholderFeeBasisPointsTier3 = 300; + coordinatorInput.shareholderFeeBasisPointsTier4 = 250; + const auto defaultFees = nostromo.getAuctionFees(); + + const auto coordinatorForbidden = nostromo.setAuctionFees(outsider, coordinatorInput); + EXPECT_EQ(coordinatorForbidden.errorCode, NOST::EAuctionError::Forbidden); + expectAuctionFeesEqual(nostromo.getAuctionFees(), defaultFees); + + NOST::SetAuctionFees_input invalidCoordinatorInput = coordinatorInput; + invalidCoordinatorInput.privateAuctionFee = -1; + const auto coordinatorInvalid = nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), invalidCoordinatorInput); + EXPECT_EQ(coordinatorInvalid.errorCode, NOST::EAuctionError::InvalidInput); + expectAuctionFeesEqual(nostromo.getAuctionFees(), defaultFees); + + const auto coordinatorSuccess = nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput); + EXPECT_EQ(coordinatorSuccess.errorCode, NOST::EAuctionError::Success); + + auto fees = nostromo.getAuctionFees(); + EXPECT_EQ(fees.privateAuctionFee, 60000000); + EXPECT_EQ(fees.auctionCancellationFeeBasisPoints, 900ULL); + EXPECT_EQ(fees.managementFeeBasisPoints, 60ULL); + EXPECT_EQ(fees.developmentFeeBasisPoints, 70ULL); + EXPECT_EQ(fees.takeoverCoordinatorFeeBasisPoints, 80ULL); + EXPECT_EQ(fees.shareholderDividendBasisPoints, 8500ULL); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier1, 400ULL); + EXPECT_EQ(fees.publicAuctionCreationFee, 123LL); + + const id managementBeforeRejectedUpdates = nostromo.getFeeRecipients().management; + const auto setManagementForbidden = nostromo.setManagement(outsider, newManagement); + EXPECT_EQ(setManagementForbidden.errorCode, NOST::EAuctionError::Forbidden); + EXPECT_EQ(nostromo.getFeeRecipients().management, managementBeforeRejectedUpdates); + + const auto setManagementInvalid = nostromo.setManagement(ContractTestingNOST::takeoverCoordinatorWallet(), NULL_ID); + EXPECT_EQ(setManagementInvalid.errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.getFeeRecipients().management, managementBeforeRejectedUpdates); + + const auto setManagementSuccess = nostromo.setManagement(ContractTestingNOST::takeoverCoordinatorWallet(), newManagement); + EXPECT_EQ(setManagementSuccess.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getFeeRecipients().management, newManagement); + + NOST::SetAuctionFeesByManagement_input managementInput{}; + managementInput.privateAuctionFee = 70000000; + managementInput.publicAuctionCreationFee = 456; + managementInput.auctionCancellationFeeBasisPoints = 800; + managementInput.managementFeeBasisPoints = 90; + managementInput.developmentFeeBasisPoints = 110; + managementInput.shareholderFeeBasisPointsTier1 = 300; + managementInput.shareholderFeeBasisPointsTier2 = 250; + managementInput.shareholderFeeBasisPointsTier3 = 200; + managementInput.shareholderFeeBasisPointsTier4 = 150; + + const auto coordinatorConfiguredFees = nostromo.getAuctionFees(); + const auto oldManagementForbidden = nostromo.setAuctionFeesByManagement(ContractTestingNOST::managementWallet(), managementInput); + EXPECT_EQ(oldManagementForbidden.errorCode, NOST::EAuctionError::Forbidden); + expectAuctionFeesEqual(nostromo.getAuctionFees(), coordinatorConfiguredFees); + + NOST::SetAuctionFeesByManagement_input invalidManagementInput = managementInput; + invalidManagementInput.managementFeeBasisPoints = 9900; + invalidManagementInput.developmentFeeBasisPoints = 200; + const auto managementInvalid = nostromo.setAuctionFeesByManagement(newManagement, invalidManagementInput); + EXPECT_EQ(managementInvalid.errorCode, NOST::EAuctionError::InvalidInput); + expectAuctionFeesEqual(nostromo.getAuctionFees(), coordinatorConfiguredFees); + + const auto managementSuccess = nostromo.setAuctionFeesByManagement(newManagement, managementInput); + EXPECT_EQ(managementSuccess.errorCode, NOST::EAuctionError::Success); + + fees = nostromo.getAuctionFees(); + EXPECT_EQ(fees.privateAuctionFee, 70000000); + EXPECT_EQ(fees.publicAuctionCreationFee, 456LL); + EXPECT_EQ(fees.auctionCancellationFeeBasisPoints, 800ULL); + EXPECT_EQ(fees.managementFeeBasisPoints, 90ULL); + EXPECT_EQ(fees.developmentFeeBasisPoints, 110ULL); + EXPECT_EQ(fees.takeoverCoordinatorFeeBasisPoints, 80ULL); + EXPECT_EQ(fees.shareholderDividendBasisPoints, 8500ULL); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier1, 300ULL); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier2, 250ULL); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier3, 200ULL); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier4, 150ULL); +} + +TEST(ContractNostromoAuction, BatchSettlementAutomaticallyFlushesLargeSellerPayoutAtEndEpochAuction) +{ + ContractTestingNOST nostromo; + const id seller(901, 902, 903, 904); + const Asset asset{seller, assetNameFromString("BIGPAY")}; + constexpr uint64 bidderCount = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL + 1ULL; + auto feeInput = nostromo.makeCoordinatorFeeInput(0); + feeInput.managementFeeBasisPoints = 0; + feeInput.developmentFeeBasisPoints = 0; + feeInput.takeoverCoordinatorFeeBasisPoints = 0; + feeInput.shareholderFeeBasisPointsTier1 = 0; + feeInput.shareholderFeeBasisPointsTier2 = 0; + feeInput.shareholderFeeBasisPointsTier3 = 0; + feeInput.shareholderFeeBasisPointsTier4 = 0; + ASSERT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), feeInput).errorCode, NOST::EAuctionError::Success); + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, static_cast(bidderCount)), static_cast(bidderCount)); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, static_cast(bidderCount)), static_cast(bidderCount)); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, bidderCount, 1)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + for (uint64 bidderIndex = 0; bidderIndex < bidderCount; ++bidderIndex) + { + const id bidder(1000 + bidderIndex, 2000 + bidderIndex, 3000 + bidderIndex, 4000 + bidderIndex); + const auto bid = nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, 1, static_cast(MAX_AMOUNT)); + ASSERT_EQ(bid.errorCode, NOST::EAuctionError::Success); + } + + const uint64 grossAmount = bidderCount * static_cast(MAX_AMOUNT); + const sint64 sellerBeforeSettlement = getBalance(seller); + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const uint64 expectedImmediatePayout = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL * static_cast(MAX_AMOUNT); + ASSERT_GT(grossAmount, expectedImmediatePayout); + EXPECT_EQ(static_cast(getBalance(seller) - sellerBeforeSettlement), expectedImmediatePayout); + EXPECT_EQ(nostromo.getPendingPayout(seller).amount, grossAmount - expectedImmediatePayout); + EXPECT_EQ(nostromo.getContractStats().stats.totalPendingQuPayouts, grossAmount - expectedImmediatePayout); + + nostromo.endEpoch(); + EXPECT_EQ(static_cast(getBalance(seller) - sellerBeforeSettlement), grossAmount); + EXPECT_EQ(nostromo.getPendingPayout(seller).amount, 0ULL); + EXPECT_EQ(nostromo.getContractStats().stats.totalPendingQuPayouts, 0ULL); +} + +TEST(ContractNostromoAuction, EndEpochPendingPayoutProcessingIsBoundedAuction) +{ + ContractTestingNOST nostromo; + constexpr uint64 recipientCount = NOST_END_EPOCH_PAYOUT_RECIPIENT_NUM + 1ULL; + + nostromo.seedUser(NOST_CONTRACT_ID, static_cast(recipientCount)); + for (uint64 recipientIndex = 0; recipientIndex < recipientCount; ++recipientIndex) + { + const id recipient(12000 + recipientIndex, 13000 + recipientIndex, 14000 + recipientIndex, 15000 + recipientIndex); + nostromo.ensureUser(recipient); + ASSERT_NE(nostromo.stateData().pendingQuPayouts.set(recipient, 1ULL), NULL_INDEX); + nostromo.stateData().totalPendingQuPayouts = sadd(nostromo.stateData().totalPendingQuPayouts, 1ULL); + } + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.stateData().pendingQuPayouts.population(), 1ULL); + EXPECT_EQ(nostromo.getContractStats().stats.totalPendingQuPayouts, 1ULL); + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.stateData().pendingQuPayouts.population(), 0ULL); + EXPECT_EQ(nostromo.getContractStats().stats.totalPendingQuPayouts, 0ULL); +} + +TEST(ContractNostromoAuction, EndEpochDoesNotRematerializeServiceFeesWhenPayoutQueueIsFullAuction) +{ + ContractTestingNOST nostromo; + const id seller(1601, 1602, 1603, 1604); + const id allowedBidder(1605, 1606, 1607, 1608); + const Asset asset{seller, assetNameFromString("FULQUE")}; + + nostromo.setRouteAllFeesToDevelopment(0); + ASSERT_EQ(nostromo.getRouteAllFeesToDevelopment(), 0); + ASSERT_EQ(nostromo.issueAsset(seller, asset.assetName, 1), 1); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); + ASSERT_EQ(nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, NOST::EAuctionError::Success); + + constexpr uint64 blockedPayoutAmount = static_cast(MAX_AMOUNT); + for (uint64 recipientIndex = 0; recipientIndex < NOST_PENDING_PAYOUT_NUM; ++recipientIndex) + { + const id recipient(20000 + recipientIndex, 30000 + recipientIndex, 40000 + recipientIndex, 50000 + recipientIndex); + ASSERT_NE(nostromo.stateData().pendingQuPayouts.set(recipient, blockedPayoutAmount), NULL_INDEX); + nostromo.stateData().totalPendingQuPayouts = sadd(nostromo.stateData().totalPendingQuPayouts, blockedPayoutAmount); + } + ASSERT_EQ(nostromo.stateData().pendingQuPayouts.population(), NOST_PENDING_PAYOUT_NUM); + + const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); + nostromo.endEpoch(); + + const auto poolAfterFirstEpoch = nostromo.getNostromoFeePool(); + EXPECT_EQ(poolAfterFirstEpoch.feePool.commonServiceFeeAmount, 0ULL); + EXPECT_EQ(poolAfterFirstEpoch.feePool.shareholderDividendAmount, 0ULL); + EXPECT_EQ(poolAfterFirstEpoch.feePool.managementAmount, 4550000ULL); + EXPECT_EQ(poolAfterFirstEpoch.feePool.developmentAmount, 4550000ULL); + EXPECT_EQ(poolAfterFirstEpoch.feePool.takeoverCoordinatorAmount, 4550000ULL); + EXPECT_EQ(poolAfterFirstEpoch.totalAmount, 13650000ULL); + EXPECT_EQ(nostromo.stateData().auctionShareholderDividendPool, 128ULL); + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()), managementBefore); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()), developmentBefore); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()), coordinatorBefore); + + nostromo.endEpoch(); + + const auto poolAfterRetry = nostromo.getNostromoFeePool(); + EXPECT_EQ(poolAfterRetry.feePool.commonServiceFeeAmount, 0ULL); + EXPECT_EQ(poolAfterRetry.feePool.managementAmount, poolAfterFirstEpoch.feePool.managementAmount); + EXPECT_EQ(poolAfterRetry.feePool.developmentAmount, poolAfterFirstEpoch.feePool.developmentAmount); + EXPECT_EQ(poolAfterRetry.feePool.takeoverCoordinatorAmount, poolAfterFirstEpoch.feePool.takeoverCoordinatorAmount); + EXPECT_EQ(poolAfterRetry.totalAmount, poolAfterFirstEpoch.totalAmount); + EXPECT_EQ(nostromo.stateData().auctionShareholderDividendPool, 128ULL); + EXPECT_EQ(nostromo.stateData().pendingQuPayouts.population(), NOST_PENDING_PAYOUT_NUM); +} + +TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) +{ + struct TierCase + { + uint64 grossAmount; + uint64 sellerPayout; + uint64 shareholderDividend; + uint64 managementFee; + uint64 developmentFee; + uint64 coordinatorFee; + uint64 totalFee; + uint64 assetName; + }; + + const TierCase cases[] = { + {5000000000ULL, 4675000000ULL, 225000000ULL, 25000000ULL, 25000000ULL, 50000000ULL, 325000000ULL, assetNameFromString("TIERA1")}, + {5000000001ULL, 4700000001ULL, 202500000ULL, 25000000ULL, 25000000ULL, 47500000ULL, 300000000ULL, assetNameFromString("TIERA2")}, + {50000000001ULL, 47250000001ULL, 1800000000ULL, 250000000ULL, 250000000ULL, 450000000ULL, 2750000000ULL, assetNameFromString("TIERA3")}, + {200000000001ULL, 190000000001ULL, 6300000000ULL, 1000000000ULL, 1000000000ULL, 1700000000ULL, 10000000000ULL, assetNameFromString("TIERA4")}, + }; + + const uint8 routeModes[] = {0, 1}; + for (uint64 caseIndex = 0; caseIndex < sizeof(cases) / sizeof(cases[0]); ++caseIndex) + { + for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) + { + SCOPED_TRACE(::testing::Message() << "caseIndex=" << caseIndex + << ", routeAllFeesToDevelopment=" << static_cast(routeModes[routeIndex])); + ContractTestingNOST nostromo; + const uint8 routeMode = routeModes[routeIndex]; + const id seller(301 + caseIndex * 2 + routeIndex, 302 + caseIndex * 2 + routeIndex, 303 + caseIndex * 2 + routeIndex, + 304 + caseIndex * 2 + routeIndex); + const id bidder(401 + caseIndex * 2 + routeIndex, 402 + caseIndex * 2 + routeIndex, 403 + caseIndex * 2 + routeIndex, + 404 + caseIndex * 2 + routeIndex); + const Asset asset{seller, cases[caseIndex].assetName}; + nostromo.setRouteAllFeesToDevelopment(routeMode); + EXPECT_EQ(nostromo.issueAsset(seller, cases[caseIndex].assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = nostromo.createAuction( + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), cases[caseIndex].grossAmount, + cases[caseIndex].grossAmount, NOST_STANDARD_MIN_BID_INCREMENT)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + nostromo.endEpoch(); + const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(cases[caseIndex].shareholderDividend); + const sint64 sellerBefore = getBalance(seller); + const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, cases[caseIndex].grossAmount, cases[caseIndex].grossAmount).errorCode, + NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + EXPECT_EQ(getBalance(seller) - sellerBefore, cases[caseIndex].sellerPayout); + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, cases[caseIndex].totalFee); + + const auto pendingFeePool = nostromo.getNostromoFeePool(); + EXPECT_EQ(pendingFeePool.totalAmount, cases[caseIndex].totalFee); + if (routeMode == 0) + { + const uint64 tierAmounts[] = { + pendingFeePool.feePool.shareholderDividendTier1Amount, pendingFeePool.feePool.shareholderDividendTier2Amount, + pendingFeePool.feePool.shareholderDividendTier3Amount, pendingFeePool.feePool.shareholderDividendTier4Amount}; + for (uint64 tierIndex = 0; tierIndex < sizeof(tierAmounts) / sizeof(tierAmounts[0]); ++tierIndex) + { + EXPECT_EQ(tierAmounts[tierIndex], tierIndex == caseIndex ? cases[caseIndex].shareholderDividend : 0ULL); + } + } + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 0ULL); + + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, cases[caseIndex].totalFee); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); + } + else + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, cases[caseIndex].managementFee); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, cases[caseIndex].developmentFee); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, cases[caseIndex].coordinatorFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); + } + } + } +} + +TEST(ContractNostromoAuction, FeeReserveGuardTriggersEmergencyPauseOnSuddenDropAuction) +{ + ContractTestingNOST nostromo; + const id seller(1001, 1002, 1003, 1004); + const id bidder(1005, 1006, 1007, 1008); + const Asset asset{seller, assetNameFromString("GRDTRG")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 5), 5); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 5), 5); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 5, 1)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + auto guardState = nostromo.getFeeReserveGuardState(); + EXPECT_EQ(guardState.isEmergencyPaused, 0); + EXPECT_EQ(guardState.dropBasisPoints, NOST_DEFAULT_FEE_RESERVE_GUARD_DROP_BP); + EXPECT_EQ(guardState.windowSeconds, NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS); + + // Drop the execution fee reserve by 20%, well past the default 10% / 10 minute guard. + const long long reserveBefore = getContractFeeReserve(NOST_CONTRACT_INDEX); + setContractFeeReserve(NOST_CONTRACT_INDEX, reserveBefore - reserveBefore / 5); + nostromo.advanceAndEndTick(1000ULL); + + guardState = nostromo.getFeeReserveGuardState(); + EXPECT_EQ(guardState.isEmergencyPaused, 1); + + const auto stats = nostromo.getContractStats(); + EXPECT_EQ(stats.stats.isEmergencyPaused, 1); + + const id newSeller(1009, 1010, 1011, 1012); + const Asset blockedAsset{newSeller, assetNameFromString("GRDBLK")}; + EXPECT_EQ(nostromo.issueAsset(newSeller, blockedAsset.assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(newSeller, blockedAsset, 2), 2); + nostromo.seedUser(newSeller, 1000); + const sint64 newSellerBefore = getBalance(newSeller); + const auto blockedCreate = nostromo.createAuctionWithFundedReward(newSeller, ContractTestingNOST::makeBatchAuctionInput(blockedAsset, 2, 1), 100); + EXPECT_EQ(blockedCreate.errorCode, NOST::EAuctionError::AuctionPaused); + EXPECT_EQ(getBalance(newSeller), newSellerBefore); + + nostromo.seedUser(bidder, 1000); + const sint64 bidderBefore = getBalance(bidder); + const auto blockedBid = nostromo.placeBidWithFundedReward(bidder, createOutput.auctionIndex, 1, 1, 100); + EXPECT_EQ(blockedBid.errorCode, NOST::EAuctionError::AuctionPaused); + EXPECT_EQ(getBalance(bidder), bidderBefore); + + const sint64 sellerBeforeCancel = getBalance(seller); + const auto blockedCancel = nostromo.cancelAuction(seller, createOutput.auctionIndex, 100); + EXPECT_EQ(blockedCancel.errorCode, NOST::EAuctionError::AuctionPaused); + // cancelAuction() seeds the reward before invoking; a paused call refunds it in full, netting the seeded amount. + EXPECT_EQ(getBalance(seller), sellerBeforeCancel + 100); + + // Non-owners cannot resume the contract. + EXPECT_EQ(nostromo.setEmergencyPause(bidder, false).errorCode, NOST::EAuctionError::Forbidden); + EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 1); + + // The coordinator can manually resume operation. + EXPECT_EQ(nostromo.setEmergencyPause(ContractTestingNOST::takeoverCoordinatorWallet(), false).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 0); + + nostromo.advanceAndEndTick(1000ULL); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); +} + +TEST(ContractNostromoAuction, FeeReserveGuardResamplesWindowWithoutFalseTriggerAuction) +{ + ContractTestingNOST nostromo; + + const long long baseline = getContractFeeReserve(NOST_CONTRACT_INDEX); + + // A gradual decline spread across multiple guard windows should never trip the single-window drop threshold. + setContractFeeReserve(NOST_CONTRACT_INDEX, baseline - baseline / 20); // -5% + nostromo.advanceAndEndTick((NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS + 10ULL) * 1000ULL); + EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 0); + + const long long afterFirstDrop = getContractFeeReserve(NOST_CONTRACT_INDEX); + setContractFeeReserve(NOST_CONTRACT_INDEX, afterFirstDrop - afterFirstDrop / 20); // another -5% + nostromo.advanceAndEndTick((NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS + 10ULL) * 1000ULL); + EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 0); +} + +TEST(ContractNostromoAuction, SetFeeReserveGuardConfigValidatesAndRestrictsCallerAuction) +{ + ContractTestingNOST nostromo; + const id stranger(1101, 1102, 1103, 1104); + const NOST::GetFeeReserveGuardState_output& defaultGuardState = nostromo.getFeeReserveGuardState(); + + EXPECT_EQ(nostromo.setFeeReserveGuardConfig(stranger, 500ULL, 300ULL).errorCode, NOST::EAuctionError::Forbidden); + + EXPECT_EQ(nostromo.setFeeReserveGuardConfig(ContractTestingNOST::takeoverCoordinatorWallet(), 0ULL, 300ULL).errorCode, + NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.setFeeReserveGuardConfig(ContractTestingNOST::takeoverCoordinatorWallet(), NOST_BASIS_POINTS_SCALE + 1ULL, 300ULL).errorCode, + NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.setFeeReserveGuardConfig(ContractTestingNOST::takeoverCoordinatorWallet(), 500ULL, 0ULL).errorCode, + NOST::EAuctionError::InvalidInput); + NOST::GetFeeReserveGuardState_output guardState = nostromo.getFeeReserveGuardState(); + EXPECT_EQ(guardState.dropBasisPoints, defaultGuardState.dropBasisPoints); + EXPECT_EQ(guardState.windowSeconds, defaultGuardState.windowSeconds); + + EXPECT_EQ(nostromo.setFeeReserveGuardConfig(ContractTestingNOST::takeoverCoordinatorWallet(), 500ULL, 300ULL).errorCode, + NOST::EAuctionError::Success); + guardState = nostromo.getFeeReserveGuardState(); + EXPECT_EQ(guardState.dropBasisPoints, 500ULL); + EXPECT_EQ(guardState.windowSeconds, 300ULL); + + EXPECT_EQ(nostromo.setFeeReserveGuardConfig(ContractTestingNOST::managementWallet(), 800ULL, 400ULL).errorCode, NOST::EAuctionError::Success); + guardState = nostromo.getFeeReserveGuardState(); + EXPECT_EQ(guardState.dropBasisPoints, 800ULL); + EXPECT_EQ(guardState.windowSeconds, 400ULL); +} + +TEST(ContractNostromoAuction, EndEpochDistributesPendingFeesWhileEmergencyPausedAuction) +{ + ContractTestingNOST nostromo; + const id seller(1201, 1202, 1203, 1204); + const Asset asset{seller, assetNameFromString("GRDEPO")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); + const NOST::CreateAuction_output& createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 3, 1)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const uint64 poolBefore = nostromo.getPendingServiceFeePool().pendingServiceFeePool; + EXPECT_GT(poolBefore, 0ULL); + + EXPECT_EQ(nostromo.setEmergencyPause(ContractTestingNOST::takeoverCoordinatorWallet(), true).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 1); + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, 0ULL); -TEST(TestContractNostromo, registerAndLogoutAndUpgradeFromTierChecker) -{ - ContractTestingNostromo nostromoTestCaseA; - - std::map duplicatedUser; - auto registers = getRandomUsers(10000, 10000); - - uint32 countOfRegister = 0, totalPoolWeight = 0; - uint64 totalDepositedQubic = 0, totalLogoutFeeAmount = 0; - - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - uint32 tierLevel = (uint32)random(1, 5); - uint64 depositeAmount, upgradeDeltaDepositeAmount; - switch (tierLevel) - { - case 1: - depositeAmount = NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT; - upgradeDeltaDepositeAmount = NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT - NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT; - totalLogoutFeeAmount += NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT * NOSTROMO_TIER_CHESTBURST_UNSTAKE_FEE / 100; - totalPoolWeight += NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; - break; - case 2: - depositeAmount = NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT; - upgradeDeltaDepositeAmount = NOSTROMO_TIER_DOG_STAKE_AMOUNT - NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT; - totalLogoutFeeAmount += NOSTROMO_TIER_DOG_STAKE_AMOUNT * NOSTROMO_TIER_DOG_UNSTAKE_FEE / 100; - totalPoolWeight += NOSTROMO_TIER_DOG_POOL_WEIGHT; - break; - case 3: - depositeAmount = NOSTROMO_TIER_DOG_STAKE_AMOUNT; - upgradeDeltaDepositeAmount = NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT - NOSTROMO_TIER_DOG_STAKE_AMOUNT; - totalLogoutFeeAmount += NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT * NOSTROMO_TIER_XENOMORPH_UNSTAKE_FEE / 100; - totalPoolWeight += NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; - break; - case 4: - depositeAmount = NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT; - upgradeDeltaDepositeAmount = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT - NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT; - totalLogoutFeeAmount += NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT * NOSTROMO_TIER_WARRIOR_UNSTAKE_FEE / 100; - totalPoolWeight += NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; - break; - case 5: - depositeAmount = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT; - totalLogoutFeeAmount += NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT * NOSTROMO_TIER_WARRIOR_UNSTAKE_FEE / 100; - totalPoolWeight += NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; - break; - default: - break; - } - // Register Tier - totalDepositedQubic += depositeAmount; - increaseEnergy(user, depositeAmount); - nostromoTestCaseA.registerInTier(user, tierLevel, depositeAmount); - nostromoTestCaseA.getState()->registerChecker(user, tierLevel, countOfRegister); - // Upgrade Tier - totalDepositedQubic += upgradeDeltaDepositeAmount; - increaseEnergy(user, upgradeDeltaDepositeAmount); - nostromoTestCaseA.upgradeTier(user, tierLevel + 1, upgradeDeltaDepositeAmount); - - if (tierLevel == 5) - { - nostromoTestCaseA.getState()->registerChecker(user, tierLevel, countOfRegister); - } - else - { - nostromoTestCaseA.getState()->registerChecker(user, tierLevel + 1, countOfRegister); - } - - duplicatedUser[user] = 1; - countOfRegister++; - } - nostromoTestCaseA.getState()->countOfRegisterChecker(countOfRegister); - nostromoTestCaseA.getState()->epochRevenueChecker(0); - nostromoTestCaseA.getState()->totalPoolWeightChecker(totalPoolWeight); - EXPECT_EQ(totalDepositedQubic, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); - - duplicatedUser.clear(); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - // Logout From Tier - nostromoTestCaseA.logoutFromTier(user); - duplicatedUser[user] = 1; - nostromoTestCaseA.getState()->logoutFromTierChecker(user); - } - EXPECT_EQ(totalLogoutFeeAmount, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); - nostromoTestCaseA.getState()->countOfRegisterChecker(0); - nostromoTestCaseA.getState()->epochRevenueChecker(totalLogoutFeeAmount); - nostromoTestCaseA.getState()->totalPoolWeightChecker(0); -} - -TEST(TestContractNostromo, createProjectAndVoteInProjectChecker) -{ - ContractTestingNostromo nostromoTestCaseB; - - auto registers = getRandomUsers(1000, 1000); - - // Register in each Tiers - increaseEnergy(registers[0], NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE); - nostromoTestCaseB.registerInTier(registers[0], 1, NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT); - - increaseEnergy(registers[1], NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE); - nostromoTestCaseB.registerInTier(registers[1], 2, NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT); - - increaseEnergy(registers[2], NOSTROMO_TIER_DOG_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE); - nostromoTestCaseB.registerInTier(registers[2], 3, NOSTROMO_TIER_DOG_STAKE_AMOUNT); - - increaseEnergy(registers[3], NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE); - nostromoTestCaseB.registerInTier(registers[3], 4, NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT); - - increaseEnergy(registers[4], NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE); - nostromoTestCaseB.registerInTier(registers[4], 5, NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT); - - setMemory(utcTime, 0); - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 12; - utcTime.Hour = 0; - updateQpiTime(); - - uint64 assetName = assetNameFromString("AAAA"); - - // This creation should be failed because there is no qualified to create the project. - nostromoTestCaseB.createProject(registers[0], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); - nostromoTestCaseB.getState()->numberOfCreatedProjectChecker(0); - nostromoTestCaseB.getState()->epochRevenueChecker(0); - EXPECT_EQ(getBalance(registers[0]), NOSTROMO_CREATE_PROJECT_FEE); - - // This creation should be failed because there is no qualified to create the project. - assetName = assetNameFromString("BBBB"); - nostromoTestCaseB.createProject(registers[1], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); - nostromoTestCaseB.getState()->numberOfCreatedProjectChecker(0); - nostromoTestCaseB.getState()->epochRevenueChecker(0); - EXPECT_EQ(getBalance(registers[1]), NOSTROMO_CREATE_PROJECT_FEE); - - // This creation should be failed because there is no qualified to create the project. - assetName = assetNameFromString("CCCC"); - nostromoTestCaseB.createProject(registers[2], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); - nostromoTestCaseB.getState()->numberOfCreatedProjectChecker(0); - nostromoTestCaseB.getState()->epochRevenueChecker(0); - EXPECT_EQ(getBalance(registers[2]), NOSTROMO_CREATE_PROJECT_FEE); - - - //This creation should be succeed because there is a qualified to create the project. - assetName = assetNameFromString("DDDD"); - nostromoTestCaseB.createProject(registers[3], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); - nostromoTestCaseB.getState()->numberOfCreatedProjectChecker(1); - nostromoTestCaseB.getState()->epochRevenueChecker(NOSTROMO_CREATE_PROJECT_FEE); - nostromoTestCaseB.getState()->createdProjectChecker(0, registers[3], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); - EXPECT_EQ(getBalance(registers[3]), 0); - - // This creation should be succeed because there is a qualified to create the project. - assetName = assetNameFromString("EEEE"); - nostromoTestCaseB.createProject(registers[4], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); - nostromoTestCaseB.getState()->numberOfCreatedProjectChecker(2); - nostromoTestCaseB.getState()->epochRevenueChecker(NOSTROMO_CREATE_PROJECT_FEE * 2); - nostromoTestCaseB.getState()->createdProjectChecker(1, registers[4], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); - EXPECT_EQ(getBalance(registers[4]), 0); - - // checkTokenCreatability function checker - EXPECT_EQ(nostromoTestCaseB.checkTokenCreatability(assetName).result, 1); - assetName = assetNameFromString("ABCD"); - EXPECT_EQ(nostromoTestCaseB.checkTokenCreatability(assetName).result, 0); - - setMemory(utcTime, 0); - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 13; - utcTime.Hour = 0; - updateQpiTime(); - - Array votedList; - - nostromoTestCaseB.voteInProject(registers[0], 0, 0); - votedList.set(0, 0); - nostromoTestCaseB.voteInProject(registers[1], 0, 1); - nostromoTestCaseB.voteInProject(registers[2], 0, 1); - nostromoTestCaseB.voteInProject(registers[3], 0, 1); - nostromoTestCaseB.voteInProject(registers[4], 0, 0); - - nostromoTestCaseB.getState()->voteInProjectChecker(0, 3, 2); - nostromoTestCaseB.getState()->numberOfVotedProjectAndVotedListChecker(registers[0], 1, votedList); - - // This vote should be failed. - nostromoTestCaseB.voteInProject(registers[0], 0, 0); - nostromoTestCaseB.getState()->voteInProjectChecker(0, 3, 2); - nostromoTestCaseB.getState()->numberOfVotedProjectAndVotedListChecker(registers[0], 1, votedList); - - // This vote should be succeed. - nostromoTestCaseB.voteInProject(registers[0], 1, 0); - votedList.set(1, 1); - nostromoTestCaseB.getState()->voteInProjectChecker(1, 0, 1); - nostromoTestCaseB.getState()->numberOfVotedProjectAndVotedListChecker(registers[0], 2, votedList); - - nostromoTestCaseB.voteInProject(registers[1], 1, 1); - nostromoTestCaseB.voteInProject(registers[2], 1, 1); - nostromoTestCaseB.voteInProject(registers[3], 1, 1); - nostromoTestCaseB.voteInProject(registers[4], 1, 1); - nostromoTestCaseB.getState()->voteInProjectChecker(1, 4, 1); -} - -TEST(TestContractNostromo, createFundraisingAndInvestInProjectAndClaimTokenChecker) -{ - uint64 epochRevenu_t = 0; - uint32 numberOfCreatedProject_t = 0; - uint32 numberOfFundraising_t = 0;; - - ContractTestingNostromo nostromoTestCaseC; - - auto registers = getRandomUsers(10000, 10000); - - setMemory(utcTime, 0); - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 11; - utcTime.Hour = 0; - updateQpiTime(); - - increaseEnergy(registers[0], NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE + NOSTROMO_QX_TOKEN_ISSUANCE_FEE); - nostromoTestCaseC.registerInTier(registers[0], 5, NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT); - uint64 assetName = assetNameFromString("GGGG"); - nostromoTestCaseC.createProject(registers[0], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); - - // getProjectByIndex function Checker - NOST::getProjectByIndex_output getProjectByIndex_output = nostromoTestCaseC.getProjectByIndex(0); - - EXPECT_EQ(getProjectByIndex_output.project.creator, registers[0]); - uint32 tmpDate; - NOST::packNostromoDate(25, 6, 15, 0, 0, 0, tmpDate); - EXPECT_EQ(getProjectByIndex_output.project.endDate , tmpDate); - EXPECT_EQ(getProjectByIndex_output.project.isCreatedFundarasing , 0); - EXPECT_EQ(getProjectByIndex_output.project.numberOfNo, 0); - EXPECT_EQ(getProjectByIndex_output.project.numberOfYes, 0); - NOST::packNostromoDate(25, 6, 13, 0, 0, 0, tmpDate); - EXPECT_EQ(getProjectByIndex_output.project.startDate, tmpDate); - EXPECT_EQ(getProjectByIndex_output.project.supplyOfToken, 21000000); - EXPECT_EQ(getProjectByIndex_output.project.tokenName, assetName); - - numberOfCreatedProject_t++; - epochRevenu_t += 100000000; - - std::map duplicatedUser; - uint64 totalPoolWeight = NOSTROMO_TIER_WARRIOR_POOL_WEIGHT, totalDepositedQubic = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT; - uint32 countOfRegister = 0; - - for (const auto& user : registers) - { - if (countOfRegister == 0) - { - countOfRegister++; - continue; - } - - if (duplicatedUser[user]) - { - continue; - } - uint8 tierLevel = (uint8)random(1, 5); - uint64 depositeAmount, userPoolWeight; - switch (tierLevel) - { - case 1: - depositeAmount = NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT; - totalPoolWeight += NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT; - userPoolWeight = NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT; - break; - case 2: - depositeAmount = NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT; - totalPoolWeight += NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; - userPoolWeight = NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; - break; - case 3: - depositeAmount = NOSTROMO_TIER_DOG_STAKE_AMOUNT; - totalPoolWeight += NOSTROMO_TIER_DOG_POOL_WEIGHT; - userPoolWeight = NOSTROMO_TIER_DOG_POOL_WEIGHT; - break; - case 4: - depositeAmount = NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT; - totalPoolWeight += NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; - userPoolWeight = NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; - break; - case 5: - depositeAmount = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT; - totalPoolWeight += NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; - userPoolWeight = NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; - break; - default: - break; - } - - // Register Tier - totalDepositedQubic += depositeAmount; - increaseEnergy(user, depositeAmount); - nostromoTestCaseC.registerInTier(user, tierLevel, depositeAmount); - - duplicatedUser[user] = 1; - countOfRegister++; - - // getTierLevelByUser function Checker - EXPECT_EQ(nostromoTestCaseC.getTierLevelByUser(user).tierLevel, tierLevel); - } - - // Vote in Project - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 14; - utcTime.Hour = 0; - updateQpiTime(); - - uint32 Ynumber = 0, Nnumber = 0; - duplicatedUser.clear(); - - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - - bit decision = (bit)random(0, 3); - if (decision) - { - Ynumber++; - } - else - { - Nnumber++; - } - - nostromoTestCaseC.voteInProject(user, 0, decision); - duplicatedUser[user] = 1; - } - nostromoTestCaseC.getState()->voteInProjectChecker(0, Ynumber, Nnumber); - - // Create the Fundraising - // This fundraising should not be created because the voting is not finished yet. - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 14; - utcTime.Hour = 0; - updateQpiTime(); - - nostromoTestCaseC.createFundraising(registers[0], 100, 2000000, 150000000, 0, - 25, 6, 17, 0, - 25, 6, 25, 0, - 25, 6, 28, 0, - 25, 7, 1, 0, - 25, 7, 10, 0, - 25, 7, 15, 0, - 25, 7, 25, 0, - 25, 7, 27, 0, - 26, 7, 27, 0, - 20, 10, 12); - - nostromoTestCaseC.getState()->countOfFundraisingChecker(0); - - // It should be created. - - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 16; - utcTime.Hour = 0; - updateQpiTime(); - - nostromoTestCaseC.createFundraising(registers[0], 100000, 2000000, 150000000000, 0, - 25, 6, 17, 0, - 25, 6, 25, 0, - 25, 6, 28, 0, - 25, 7, 1, 0, - 25, 7, 10, 0, - 25, 7, 15, 0, - 25, 7, 25, 0, - 25, 7, 27, 0, - 26, 7, 27, 0, - 20, 10, 12); - numberOfFundraising_t++; - - nostromoTestCaseC.getState()->countOfFundraisingChecker(1); - nostromoTestCaseC.getState()->createFundraisingChecker(registers[0], 100000, 2000000, 150000000000, 0, - 25, 6, 17, 0, - 25, 6, 25, 0, - 25, 6, 28, 0, - 25, 7, 1, 0, - 25, 7, 10, 0, - 25, 7, 15, 0, - 25, 7, 25, 0, - 25, 7, 27, 0, - 26, 7, 27, 0, - 20, 10, 12, 0); - - // getFundarasingByIndex function checker - NOST::getFundarasingByIndex_output getFundarasingByIndex_output = nostromoTestCaseC.getFundarasingByIndex(0); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.indexOfProject, 0); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.isCreatedToken, 0); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.raisedFunds, 0); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.requiredFunds, 150000000000); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.soldAmount, 2000000); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.stepOfVesting, 12); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.TGE, 10); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.threshold, 20); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.tokenPrice, 100000); - NOST::packNostromoDate(25, 6, 17, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.firstPhaseStartDate, tmpDate); - NOST::packNostromoDate(25, 6, 25, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.firstPhaseEndDate, tmpDate); - NOST::packNostromoDate(25, 6, 28, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.secondPhaseStartDate, tmpDate); - NOST::packNostromoDate(25, 7, 1, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.secondPhaseEndDate, tmpDate); - NOST::packNostromoDate(25, 7, 10, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.thirdPhaseStartDate, tmpDate); - NOST::packNostromoDate(25, 7, 15, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.thirdPhaseEndDate, tmpDate); - NOST::packNostromoDate(25, 7, 25, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.listingStartDate, tmpDate); - NOST::packNostromoDate(25, 7, 27, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.cliffEndDate, tmpDate); - NOST::packNostromoDate(26, 7, 27, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.vestingEndDate, tmpDate); - - // Phase 1 Investment - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 17; - utcTime.Hour = 1; - updateQpiTime(); - - uint64 facehuggerMaxInvestAmount = 180000000000 * NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT / totalPoolWeight; - uint64 chestburstMaxInvestAmount = 180000000000 * NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT / totalPoolWeight; - uint64 dogMaxInvestAmount = 180000000000 * NOSTROMO_TIER_DOG_POOL_WEIGHT / totalPoolWeight; - uint64 xenomorphMaxInvestAmount = 180000000000 * NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT / totalPoolWeight; - uint64 warriorMaxInvestAmount = 180000000000 * NOSTROMO_TIER_WARRIOR_POOL_WEIGHT / totalPoolWeight; - - uint64 totalInvestedAmount = 0; - duplicatedUser.clear(); - uint32 ct = 0; - uint32 overDeposit = 1000; // it should be ignored - uint64 originalSCBalance = getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0)); - - std::map investedAmountMP; - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - ct++; - continue; - } - ct++; - increaseEnergy(user, 180000000000); - uint8 tierLevel = nostromoTestCaseC.getState()->getTierLevel(user); - - if (ct >= 4000) - { - // Phase 2 Investment - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 29; - utcTime.Hour = 0; - updateQpiTime(); - } - - switch (tierLevel) - { - case 1: - if (ct < 4000) - { - totalInvestedAmount += facehuggerMaxInvestAmount; - investedAmountMP[user] += facehuggerMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 0, facehuggerMaxInvestAmount + overDeposit); - break; - case 2: - if (ct < 4000) - { - totalInvestedAmount += chestburstMaxInvestAmount; - investedAmountMP[user] += chestburstMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 0, chestburstMaxInvestAmount + overDeposit); - break; - case 3: - if (ct < 4000) - { - totalInvestedAmount += dogMaxInvestAmount; - investedAmountMP[user] += dogMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 0, dogMaxInvestAmount + overDeposit); - break; - case 4: - totalInvestedAmount += xenomorphMaxInvestAmount; - investedAmountMP[user] += xenomorphMaxInvestAmount; - nostromoTestCaseC.investInProject(user, 0, xenomorphMaxInvestAmount + overDeposit); - break; - case 5: - totalInvestedAmount += warriorMaxInvestAmount; - investedAmountMP[user] += warriorMaxInvestAmount; - nostromoTestCaseC.investInProject(user, 0, warriorMaxInvestAmount + overDeposit); - break; - - default: - break; - } - - duplicatedUser[user] = 1; - } - - nostromoTestCaseC.getState()->totalRaisedFundChecker(0, totalInvestedAmount, assetName); - EXPECT_EQ(originalSCBalance + totalInvestedAmount - NOSTROMO_QX_TOKEN_ISSUANCE_FEE, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); - - // Phase 3 Investment - utcTime.Year = 2025; - utcTime.Month = 7; - utcTime.Day = 11; - utcTime.Hour = 0; - updateQpiTime(); - - uint64 amount = 10000000; - duplicatedUser.clear(); - ct = 0; - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - ct++; - uint8 tierLevel = nostromoTestCaseC.getState()->getTierLevel(user); - increaseEnergy(user, amount); - nostromoTestCaseC.investInProject(user, 0, amount); - if (totalInvestedAmount + amount < 180000000000) - { - totalInvestedAmount += amount; - investedAmountMP[user] += amount; - - // getNumberOfInvestedProjects function checker - NOST::getNumberOfInvestedProjects_output getNumberOfInvestedProjects_output = nostromoTestCaseC.getNumberOfInvestedProjects(user); - - EXPECT_EQ(getNumberOfInvestedProjects_output.numberOfInvestedProjects, 1); - } - duplicatedUser[user] = 1; - } - - nostromoTestCaseC.getState()->totalRaisedFundChecker(0, totalInvestedAmount, assetName); - EXPECT_EQ(originalSCBalance + totalInvestedAmount - NOSTROMO_QX_TOKEN_ISSUANCE_FEE, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); - - // getMaxClaimAmount function checker - utcTime.Year = 2025; - utcTime.Month = 7; - utcTime.Day = 26; - utcTime.Hour = 0; - updateQpiTime(); - - duplicatedUser.clear(); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - - EXPECT_EQ(nostromoTestCaseC.getMaxClaimAmount(user, 0), investedAmountMP[user] / 100000 * 10 / 100); - - duplicatedUser[user] = 1; - } - - utcTime.Year = 2025; - utcTime.Month = 8; - utcTime.Day = 5; - utcTime.Hour = 0; - updateQpiTime(); - - duplicatedUser.clear(); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - - EXPECT_EQ(nostromoTestCaseC.getMaxClaimAmount(user, 0), investedAmountMP[user] / 100000 * (10 + 7) / 100); - - duplicatedUser[user] = 1; - } - - utcTime.Year = 2026; - utcTime.Month = 8; - utcTime.Day = 5; - utcTime.Hour = 0; - updateQpiTime(); - - duplicatedUser.clear(); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - - EXPECT_EQ(nostromoTestCaseC.getMaxClaimAmount(user, 0), investedAmountMP[user] / 100000); - - duplicatedUser[user] = 1; - } - - // claimToken Checker - std::map claimedAmountMP; - for (uint32 i = 1; i <= 12; i++) - { - if (i >= 6) - { - utcTime.Year = 2026; - } - utcTime.Month = (7 + i) % 12; - if (utcTime.Month == 0) utcTime.Month = 12; - utcTime.Day = 5; - utcTime.Hour = 0; - updateQpiTime(); - - duplicatedUser.clear(); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - - uint64 investedAmount = nostromoTestCaseC.getState()->getInvestedAmount(0, user); - uint64 claimAmount = investedAmount / 100000 / 12; - claimedAmountMP[user] += nostromoTestCaseC.claimToken(user, claimAmount, 0); - - duplicatedUser[user] = 1; - } - } - - // getInfoUserInvested function checker - duplicatedUser.clear(); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - - duplicatedUser[user] = 1; - - NOST::getInfoUserInvested_output getInfoUserInvested_output = nostromoTestCaseC.getInfoUserInvested(user); - EXPECT_EQ(getInfoUserInvested_output.listUserInvested.get(0).indexOfFundraising, 0); - EXPECT_EQ(getInfoUserInvested_output.listUserInvested.get(0).investedAmount, investedAmountMP[user]); - EXPECT_EQ(getInfoUserInvested_output.listUserInvested.get(0).claimedAmount, claimedAmountMP[user]); - } - - // Checking to remove element after claiming the max amount - utcTime.Year = 2026; - utcTime.Month = 8; - utcTime.Day = 5; - utcTime.Hour = 0; - updateQpiTime(); - - duplicatedUser.clear(); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - uint64 claimAmount = nostromoTestCaseC.getMaxClaimAmount(user, 0) - claimedAmountMP[user]; - claimedAmountMP[user] += nostromoTestCaseC.claimToken(user, claimAmount, 0); - - duplicatedUser[user] = 1; - - nostromoTestCaseC.getState()->removeElementAfterClaimChecker(user); - } - - ct = 0; - duplicatedUser.clear(); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - if (ct == 0) - { - EXPECT_EQ(numberOfPossessedShares(assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), user, user, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX) - 19000000, claimedAmountMP[user]); - } - else - { - EXPECT_EQ(numberOfPossessedShares(assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), user, user, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX), claimedAmountMP[user]); - } - ct++; - duplicatedUser[user] = 1; - } - - // transferShareManagementRights Checker - increaseEnergy(registers[0], 1000000); - - Asset asset; - asset.assetName = assetName; - asset.issuer = id(NOST_CONTRACT_INDEX, 0, 0, 0); - EXPECT_EQ(nostromoTestCaseC.TransferShareManagementRights(registers[0], asset, 10000, QX_CONTRACT_INDEX), 10000); - EXPECT_EQ(numberOfPossessedShares(asset.assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), registers[0], registers[0], QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), 10000); - - // EndEpochSucceedFundraising Checker - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 20; - utcTime.Hour = 0; - updateQpiTime(); - - increaseEnergy(registers[0], NOSTROMO_CREATE_PROJECT_FEE); - assetName = assetNameFromString("AAAA"); - nostromoTestCaseC.createProject(registers[0], assetName, 21000000, 25, 6, 22, 0, 25, 6, 25, 0); - numberOfCreatedProject_t++; - epochRevenu_t += 100000000; - - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 23; - utcTime.Hour = 0; - updateQpiTime(); - - Ynumber = 0; Nnumber = 0; - duplicatedUser.clear(); - - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - - bit decision = (bit)random(0, 3); - if (decision) - { - Ynumber++; - } - else - { - Nnumber++; - } - - nostromoTestCaseC.voteInProject(user, 1, decision); - duplicatedUser[user] = 1; - - // getUserVoteStatus function Checker - NOST::getUserVoteStatus_output getUserVoteStatus_output = nostromoTestCaseC.getUserVoteStatus(user); - EXPECT_EQ(getUserVoteStatus_output.numberOfVotedProjects, 2); - EXPECT_EQ(getUserVoteStatus_output.projectIndexList.get(0), 0); - EXPECT_EQ(getUserVoteStatus_output.projectIndexList.get(1), 1); - } - nostromoTestCaseC.getState()->voteInProjectChecker(1, Ynumber, Nnumber); - - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 26; - utcTime.Hour = 0; - updateQpiTime(); - increaseEnergy(registers[0], NOSTROMO_QX_TOKEN_ISSUANCE_FEE); - - nostromoTestCaseC.createFundraising(registers[0], 100000, 2000000, 150000000000, 1, - 25, 6, 27, 0, - 25, 7, 5, 0, - 25, 7, 8, 0, - 25, 7, 10, 0, - 25, 7, 20, 0, - 25, 7, 23, 0, - 25, 7, 25, 0, - 25, 7, 27, 0, - 26, 7, 27, 0, - 20, 10, 12); - numberOfFundraising_t++; - - nostromoTestCaseC.getState()->countOfFundraisingChecker(2); - nostromoTestCaseC.getState()->createFundraisingChecker(registers[0], 100000, 2000000, 150000000000, 1, - 25, 6, 27, 0, - 25, 7, 5, 0, - 25, 7, 8, 0, - 25, 7, 10, 0, - 25, 7, 20, 0, - 25, 7, 23, 0, - 25, 7, 25, 0, - 25, 7, 27, 0, - 26, 7, 27, 0, - 20, 10, 12, 1); - - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 27; - utcTime.Hour = 1; - updateQpiTime(); - - uint64 totalInvestedAmount_2 = 0; - duplicatedUser.clear(); - ct = 0; - originalSCBalance = getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0)); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - ct++; - continue; - } - ct++; - increaseEnergy(user, 180000000000); - uint8 tierLevel = nostromoTestCaseC.getState()->getTierLevel(user); - - if (ct >= 4000) - { - - // Phase 2 Investment - utcTime.Year = 2025; - utcTime.Month = 7; - utcTime.Day = 9; - utcTime.Hour = 0; - updateQpiTime(); - } - - switch (tierLevel) - { - case 1: - if (ct < 4000) - { - totalInvestedAmount_2 += facehuggerMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 1, facehuggerMaxInvestAmount); - break; - case 2: - if (ct < 4000) - { - totalInvestedAmount_2 += chestburstMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 1, chestburstMaxInvestAmount); - break; - case 3: - if (ct < 4000) - { - totalInvestedAmount_2 += dogMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 1, dogMaxInvestAmount); - break; - case 4: - totalInvestedAmount_2 += xenomorphMaxInvestAmount; - nostromoTestCaseC.investInProject(user, 1, xenomorphMaxInvestAmount); - break; - case 5: - totalInvestedAmount_2 += warriorMaxInvestAmount; - nostromoTestCaseC.investInProject(user, 1, warriorMaxInvestAmount); - break; - - default: - break; - } - - duplicatedUser[user] = 1; - } - - nostromoTestCaseC.getState()->totalRaisedFundChecker(1, totalInvestedAmount_2, assetName); - EXPECT_EQ(originalSCBalance + totalInvestedAmount_2 - NOSTROMO_QX_TOKEN_ISSUANCE_FEE, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); - - // getStats function Checker - nostromoTestCaseC.getState()->getStatsChecker(epochRevenu_t, totalPoolWeight, numberOfCreatedProject_t, numberOfFundraising_t, countOfRegister); - - utcTime.Year = 2025; - utcTime.Month = 7; - utcTime.Day = 24; - utcTime.Hour = 0; - updateQpiTime(); - - uint64 originalCreatorBalance = getBalance(registers[0]); - nostromoTestCaseC.endEpoch(); - EXPECT_EQ(getBalance(registers[0]) - originalCreatorBalance, totalInvestedAmount - div(totalInvestedAmount * 5, 100ULL) + totalInvestedAmount_2 - div(totalInvestedAmount_2 * 5, 100ULL)); - nostromoTestCaseC.getState()->endEpochSucceedFundraisingChecker(registers[0], 1, totalInvestedAmount_2, originalCreatorBalance, assetName); - - // EndEpochFailedFundraising Checker - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 20; - utcTime.Hour = 0; - updateQpiTime(); - - increaseEnergy(registers[0], NOSTROMO_CREATE_PROJECT_FEE); - assetName = assetNameFromString("BBBB"); - nostromoTestCaseC.createProject(registers[0], assetName, 21000000, 25, 6, 22, 0, 25, 6, 25, 0); - numberOfCreatedProject_t++; - epochRevenu_t += 100000000; - - // getProjectIndexListByCreator function checker - NOST::getProjectIndexListByCreator_output getProjectIndexListByCreator_output = nostromoTestCaseC.getProjectIndexListByCreator(registers[0]); - for (uint32 i = 0; i < 128; i++) - { - if (i < 3) - { - EXPECT_EQ(getProjectIndexListByCreator_output.indexListForProjects.get(i), i); - } - else { - EXPECT_EQ(getProjectIndexListByCreator_output.indexListForProjects.get(i), 262144); - } - } - - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 23; - utcTime.Hour = 0; - updateQpiTime(); - - Ynumber = 0; Nnumber = 0; - duplicatedUser.clear(); - - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - - bit decision = (bit)random(0, 3); - if (decision) - { - Ynumber++; - } - else - { - Nnumber++; - } - - nostromoTestCaseC.voteInProject(user, 2, decision); - duplicatedUser[user] = 1; - } - nostromoTestCaseC.getState()->voteInProjectChecker(2, Ynumber, Nnumber); - - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 26; - utcTime.Hour = 0; - updateQpiTime(); - increaseEnergy(registers[0], NOSTROMO_QX_TOKEN_ISSUANCE_FEE); - - nostromoTestCaseC.createFundraising(registers[0], 100000, 2000000, 150000000000, 2, - 25, 6, 27, 0, - 25, 7, 5, 0, - 25, 7, 8, 0, - 25, 7, 10, 0, - 25, 7, 20, 0, - 25, 7, 23, 0, - 25, 7, 25, 0, - 25, 7, 27, 0, - 26, 7, 27, 0, - 20, 10, 12); - numberOfFundraising_t++; - - nostromoTestCaseC.getState()->countOfFundraisingChecker(3); - nostromoTestCaseC.getState()->createFundraisingChecker(registers[0], 100000, 2000000, 150000000000, 2, - 25, 6, 27, 0, - 25, 7, 5, 0, - 25, 7, 8, 0, - 25, 7, 10, 0, - 25, 7, 20, 0, - 25, 7, 23, 0, - 25, 7, 25, 0, - 25, 7, 27, 0, - 26, 7, 27, 0, - 20, 10, 12, 2); - - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 27; - utcTime.Hour = 1; - updateQpiTime(); - - uint64 totalInvestedAmount_3 = 0; - duplicatedUser.clear(); - ct = 0; - originalSCBalance = getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0)); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - ct++; - continue; - } - ct++; - increaseEnergy(user, 180000000000); - uint8 tierLevel = nostromoTestCaseC.getState()->getTierLevel(user); - - if (ct >= 4000) - { - - // Phase 2 Investment - utcTime.Year = 2025; - utcTime.Month = 7; - utcTime.Day = 9; - utcTime.Hour = 0; - updateQpiTime(); - } - - bit sg = 0; - switch (tierLevel) - { - case 1: - if (ct < 4000) - { - if (totalInvestedAmount_3 + facehuggerMaxInvestAmount > 120000000000) - { - sg = 1; - break; - } - totalInvestedAmount_3 += facehuggerMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 2, facehuggerMaxInvestAmount); - break; - case 2: - if (ct < 4000) - { - if (totalInvestedAmount_3 + chestburstMaxInvestAmount > 120000000000) - { - sg = 1; - break; - } - totalInvestedAmount_3 += chestburstMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 2, chestburstMaxInvestAmount); - break; - case 3: - if (ct < 4000) - { - if (totalInvestedAmount_3 + dogMaxInvestAmount > 120000000000) - { - sg = 1; - break; - } - totalInvestedAmount_3 += dogMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 2, dogMaxInvestAmount); - break; - case 4: - if (totalInvestedAmount_3 + xenomorphMaxInvestAmount > 120000000000) - { - sg = 1; - break; - } - totalInvestedAmount_3 += xenomorphMaxInvestAmount; - nostromoTestCaseC.investInProject(user, 2, xenomorphMaxInvestAmount); - break; - case 5: - if (totalInvestedAmount_3 + warriorMaxInvestAmount > 120000000000) - { - sg = 1; - break; - } - totalInvestedAmount_3 += warriorMaxInvestAmount; - nostromoTestCaseC.investInProject(user, 2, warriorMaxInvestAmount); - break; - - default: - break; - } - - if (sg) - { - break; - } - - duplicatedUser[user] = 1; - } - - nostromoTestCaseC.getState()->totalRaisedFundChecker(2, totalInvestedAmount_3, assetName); - - utcTime.Year = 2025; - utcTime.Month = 7; - utcTime.Day = 24; - utcTime.Hour = 0; - updateQpiTime(); - - originalCreatorBalance = getBalance(registers[0]); - EXPECT_EQ(originalSCBalance + totalInvestedAmount_3, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); - - uint64 epochRevenue = nostromoTestCaseC.getState()->getEpochRevenue(); - uint64 teamFee = div(epochRevenue, 10ULL); - epochRevenue -= teamFee; - nostromoTestCaseC.endEpoch(); - - EXPECT_EQ(originalSCBalance + totalInvestedAmount_3 - teamFee - (div(epochRevenue, 676ULL) * 676), getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); - nostromoTestCaseC.getState()->endEpochFailedFundraisingChecker(2); - nostromoTestCaseC.getState()->endEpochVoteStatusClearChecker(); + // The auction should remain paused after END_EPOCH; only a manual resume clears it. + EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 1); } From 2dc7531bf924785d406c1d95f3bbe6aa892acd25 Mon Sep 17 00:00:00 2001 From: fnordspace Date: Tue, 1 Sep 2026 12:16:42 +0200 Subject: [PATCH 11/21] update params for epoch229 / v1.303.0 --- src/public_settings.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/public_settings.h b/src/public_settings.h index c1e3c024..66d53cd8 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -73,12 +73,12 @@ static_assert(AUTO_FORCE_NEXT_TICK_THRESHOLD* TARGET_TICK_DURATION >= PEER_REFRE // Config options that should NOT be changed by operators #define VERSION_A 1 -#define VERSION_B 302 -#define VERSION_C 1 +#define VERSION_B 303 +#define VERSION_C 0 // Epoch and initial tick for node startup -#define EPOCH 228 -#define TICK 76550000 +#define EPOCH 229 +#define TICK 77700000 #define TICK_IS_FIRST_TICK_OF_EPOCH 1 // Set to 0 if the network is restarted during the EPOCH with a new initial TICK #define ARBITRATOR "AFZPUAIYVPNUYGJRQVLUKOPPVLHAZQTGLYAAUUNBXFTVTAMSBKQBLEIEPCVJ" From 409d1c4ed0cd82e2eea8756b8068eae9add06e4b Mon Sep 17 00:00:00 2001 From: fnordspace Date: Tue, 1 Sep 2026 12:25:54 +0200 Subject: [PATCH 12/21] Remove Qswap_old.h and toggle --- src/Qubic.vcxproj | 1 - src/Qubic.vcxproj.filters | 3 - src/contract_core/contract_def.h | 4 - src/contracts/Qswap_old.h | 2436 ------------------------------ 4 files changed, 2444 deletions(-) delete mode 100644 src/contracts/Qswap_old.h diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index db075290..04add729 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -26,7 +26,6 @@ - diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index 0c2a0070..10cc5247 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -242,9 +242,6 @@ contracts - - contracts - platform diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index c8d81ba6..300adc1a 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -145,11 +145,7 @@ #define CONTRACT_INDEX QSWAP_CONTRACT_INDEX #define CONTRACT_STATE_TYPE QSWAP #define CONTRACT_STATE2_TYPE QSWAP2 -#ifdef OLD_QSWAP -#include "contracts/Qswap_old.h" -#else #include "contracts/Qswap.h" -#endif #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE diff --git a/src/contracts/Qswap_old.h b/src/contracts/Qswap_old.h deleted file mode 100644 index ecf63457..00000000 --- a/src/contracts/Qswap_old.h +++ /dev/null @@ -1,2436 +0,0 @@ -using namespace QPI; - -// Log types enum for QSWAP contract -enum QSWAPLogInfo { - QSWAPAddLiquidity = 4, - QSWAPRemoveLiquidity = 5, - QSWAPSwapExactQuForAsset = 6, - QSWAPSwapQuForExactAsset = 7, - QSWAPSwapExactAssetForQu = 8, - QSWAPSwapAssetForExactQu = 9, - QSWAPFailedDistribution = 10, -}; - -// FIXED CONSTANTS -constexpr uint64 QSWAP_INITIAL_MAX_POOL = 16384; -constexpr uint64 QSWAP_MAX_POOL = QSWAP_INITIAL_MAX_POOL * X_MULTIPLIER; -constexpr uint64 QSWAP_MAX_USER_PER_POOL = 256; -constexpr sint64 QSWAP_MIN_LIQUIDITY = 1000; -constexpr uint32 QSWAP_SWAP_FEE_BASE = 10000; -constexpr uint32 QSWAP_FEE_BASE_100 = 100; - -struct QSWAP2 -{ -}; - -struct QSWAP : public ContractBase -{ -public: - // Logging message structures - struct AddLiquidityMessage - { - uint32 _contractIndex; - uint32 _type; - id assetIssuer; - uint64 assetName; - sint64 userIncreaseLiquidity; - sint64 quAmount; - sint64 assetAmount; - sint8 _terminator; - }; - - struct RemoveLiquidityMessage - { - uint32 _contractIndex; - uint32 _type; - sint64 quAmount; - sint64 assetAmount; - sint8 _terminator; - }; - - struct SwapMessage - { - uint32 _contractIndex; - uint32 _type; - id assetIssuer; - uint64 assetName; - sint64 assetAmountIn; - sint64 assetAmountOut; - sint8 _terminator; - }; - - struct FailedDistributionMessage - { - uint32 _contractIndex; - uint32 _type; - id dst; - uint64 amount; - sint8 _terminator; - }; - - // Types used by state fields - struct PoolBasicState - { - id poolID; - sint64 reservedQuAmount; - sint64 reservedAssetAmount; - sint64 totalLiquidity; - }; - - struct LiquidityInfo - { - id entity; - sint64 liquidity; - }; - - struct StateData - { - uint32 swapFeeRate; // e.g. 30: 0.3% (base: 10_000) - uint32 investRewardsFeeRate;// 3: 3% of swap fees to Invest & Rewards (base: 100) - uint32 shareholderFeeRate; // 27: 27% of swap fees to SC shareholders (base: 100) - uint32 poolCreationFeeRate; // e.g. 10: 10% (base: 100) - - id investRewardsId; - uint64 investRewardsEarnedFee; - uint64 investRewardsDistributedAmount; - - uint64 shareholderEarnedFee; - uint64 shareholderDistributedAmount; - - Array mPoolBasicStates; - Collection mLiquidities; - - uint32 qxFeeRate; // 5: 5% of swap fees to QX (base: 100) - uint32 burnFeeRate; // 1: 1% of swap fees burned (base: 100) - - uint64 qxEarnedFee; - uint64 qxDistributedAmount; - - uint64 burnEarnedFee; // Total burn fees collected (to be burned in END_TICK) - uint64 burnedAmount; // Total amount actually burned - - uint32 cachedIssuanceFee; - uint32 cachedTransferFee; - }; - - struct Fees_input - { - }; - struct Fees_output - { - uint32 assetIssuanceFee; // Amount of qus - uint32 poolCreationFee; // Amount of qus - uint32 transferFee; // Amount of qus - - uint32 swapFee; // 30 -> 0.3% - uint32 shareholderFee; // 27 -> 27% of swap fee, for SC shareholders - uint32 investRewardsFee; // 3 -> 3% of swap fee, for Invest & Rewards - uint32 qxFee; // 5 -> 5% of swap fee, for QX - uint32 burnFee; // 1 -> 1% of swap fee, burned - }; - - struct InvestRewardsInfo_input - { - }; - struct InvestRewardsInfo_output - { - uint32 investRewardsFee; // 3 -> 3% of swap fee - id investRewardsId; - }; - - struct SetInvestRewardsInfo_input - { - id newInvestRewardsId; - }; - struct SetInvestRewardsInfo_output - { - bit success; - }; - - struct GetPoolBasicState_input - { - id assetIssuer; - uint64 assetName; - }; - struct GetPoolBasicState_output - { - sint64 poolExists; - sint64 reservedQuAmount; - sint64 reservedAssetAmount; - sint64 totalLiquidity; - }; - - struct GetLiquidityOf_input - { - id assetIssuer; - uint64 assetName; - id account; - }; - struct GetLiquidityOf_output - { - sint64 liquidity; - }; - - struct QuoteExactQuInput_input - { - id assetIssuer; - uint64 assetName; - sint64 quAmountIn; - }; - struct QuoteExactQuInput_output - { - sint64 assetAmountOut; - }; - - struct QuoteExactQuOutput_input{ - id assetIssuer; - uint64 assetName; - sint64 quAmountOut; - }; - struct QuoteExactQuOutput_output - { - sint64 assetAmountIn; - }; - - struct QuoteExactAssetInput_input - { - id assetIssuer; - uint64 assetName; - sint64 assetAmountIn; - }; - struct QuoteExactAssetInput_output - { - sint64 quAmountOut; - }; - - struct QuoteExactAssetOutput_input - { - id assetIssuer; - uint64 assetName; - sint64 assetAmountOut; - }; - struct QuoteExactAssetOutput_output - { - sint64 quAmountIn; - }; - - struct IssueAsset_input - { - uint64 assetName; - sint64 numberOfShares; - uint64 unitOfMeasurement; - sint8 numberOfDecimalPlaces; - }; - struct IssueAsset_output - { - sint64 issuedNumberOfShares; - }; - - struct CreatePool_input - { - id assetIssuer; - uint64 assetName; - }; - struct CreatePool_output - { - bit success; - }; - - struct TransferShareOwnershipAndPossession_input - { - id assetIssuer; - uint64 assetName; - id newOwnerAndPossessor; - sint64 amount; - }; - struct TransferShareOwnershipAndPossession_output - { - sint64 transferredAmount; - }; - - /** - * @param quAmountADesired The amount of tokenA to add as liquidity if the B/A price is <= amountBDesired/amountADesired (A depreciates). - * @param assetAmountBDesired The amount of tokenB to add as liquidity if the A/B price is <= amountADesired/amountBDesired (B depreciates). - * @param quAmountMin Bounds the extent to which the B/A price can go up before the transaction reverts. Must be <= amountADesired. - * @param assetAmountMin Bounds the extent to which the A/B price can go up before the transaction reverts. Must be <= amountBDesired. - */ - struct AddLiquidity_input - { - id assetIssuer; - uint64 assetName; - sint64 assetAmountDesired; - sint64 quAmountMin; - sint64 assetAmountMin; - }; - struct AddLiquidity_output - { - sint64 userIncreaseLiquidity; - sint64 quAmount; - sint64 assetAmount; - }; - - struct RemoveLiquidity_input - { - id assetIssuer; - uint64 assetName; - sint64 burnLiquidity; - sint64 quAmountMin; - sint64 assetAmountMin; - }; - - struct RemoveLiquidity_output - { - sint64 quAmount; - sint64 assetAmount; - }; - - struct SwapExactQuForAsset_input - { - id assetIssuer; - uint64 assetName; - sint64 assetAmountOutMin; - }; - struct SwapExactQuForAsset_output - { - sint64 assetAmountOut; - }; - - struct SwapQuForExactAsset_input - { - id assetIssuer; - uint64 assetName; - sint64 assetAmountOut; - }; - struct SwapQuForExactAsset_output - { - sint64 quAmountIn; - }; - - struct SwapExactAssetForQu_input - { - id assetIssuer; - uint64 assetName; - sint64 assetAmountIn; - sint64 quAmountOutMin; - }; - struct SwapExactAssetForQu_output - { - sint64 quAmountOut; - }; - - struct SwapAssetForExactQu_input - { - id assetIssuer; - uint64 assetName; - sint64 assetAmountInMax; - sint64 quAmountOut; - }; - struct SwapAssetForExactQu_output - { - sint64 assetAmountIn; - }; - - struct TransferShareManagementRights_input - { - Asset asset; - sint64 numberOfShares; - uint32 newManagingContractIndex; - }; - struct TransferShareManagementRights_output - { - sint64 transferredNumberOfShares; - }; - -protected: - - - inline static sint64 min(sint64 a, sint64 b) - { - return (a < b) ? a : b; - } - - // find the sqrt of a*b - inline static sint64 sqrt(sint64& a, sint64& b, uint128& prod, uint128& y, uint128& z) - { - if (a == b) - { - return a; - } - - prod = uint128(a) * uint128(b); - - y = uint128(0); - - z = uint128(1) << uint128(0, 126); - while (z > prod) - { - z >>= uint128(0, 2); - } - - while (z) - { - if (prod >= y + z) - { - prod -= y + z; - y = (y >> uint128(0, 1)) + z; - } - else - { - y >>= uint128(0, 1); - } - z >>= uint128(0, 2); - } - - return sint64(y.low); - } - - inline static sint64 quoteEquivalentAmountB(sint64& amountADesired, sint64& reserveA, sint64& reserveB, uint128& tmpRes) - { - // amountDesired * reserveB / reserveA - tmpRes = div(uint128(amountADesired) * uint128(reserveB), uint128(reserveA)); - - if ((tmpRes.high != 0)|| (tmpRes.low > 0x7FFFFFFFFFFFFFFF)) - { - return -1; - } - else - { - return sint64(tmpRes.low); - } - } - - // reserveIn * reserveOut = (reserveIn + amountIn * (1-fee)) * (reserveOut - x) - // x = reserveOut * amountIn * (1-fee) / (reserveIn + amountIn * (1-fee)) - inline static sint64 getAmountOutTakeFeeFromInToken( - sint64& amountIn, - sint64& reserveIn, - sint64& reserveOut, - uint32 fee, - uint128& amountInWithFee, - uint128& numerator, - uint128& denominator, - uint128& tmpRes - ) - { - if (amountIn >= MAX_AMOUNT) return -1; - - amountInWithFee = uint128(amountIn) * uint128(QSWAP_SWAP_FEE_BASE - fee); - numerator = uint128(reserveOut) * amountInWithFee; - denominator = uint128(reserveIn) * uint128(QSWAP_SWAP_FEE_BASE) + amountInWithFee; - - // numerator / denominator - tmpRes = div(numerator, denominator); - if ((tmpRes.high != 0) || (tmpRes.low > 0x7FFFFFFFFFFFFFFF)) - { - return -1; - } - else - { - return sint64(tmpRes.low); - } - } - - // reserveIn * reserveOut = (reserveIn + x * (1-fee)) * (reserveOut - amountOut) - // x = (reserveIn * amountOut * 10000) / ((reserveOut - amountOut) * (10000 - fee)) - inline static sint64 getAmountInTakeFeeFromInToken(sint64& amountOut, sint64& reserveIn, sint64& reserveOut, uint32 fee, uint128& numerator, uint128& denominator, uint128& tmpRes) - { - if (amountOut >= MAX_AMOUNT) return -1; - - // Calculate full numerator first to avoid premature truncation - numerator = uint128(reserveIn) * uint128(amountOut) * uint128(QSWAP_SWAP_FEE_BASE); - denominator = uint128(reserveOut - amountOut) * uint128(QSWAP_SWAP_FEE_BASE - fee); - - // Perform single division at the end - // Use floor + 1 to ensure user pays at least enough (protects LPs) - tmpRes = div(numerator, denominator) + uint128(1); - - if ((tmpRes.high != 0) || (tmpRes.low > 0x7FFFFFFFFFFFFFFF)) - { - return -1; - } - else - { - return sint64(tmpRes.low); - } - } - - // (reserveIn + amountIn) * (reserveOut - x) = reserveIn * reserveOut - // x = reserveOut * amountIn / (reserveIn + amountIn) - // NOTE: Despite the name, this returns the GROSS output (before fee deduction). - // The fee parameter is unused here because fee is applied separately by the caller. - // This is intentional: the caller needs the gross value for fee distribution calculation. - inline static sint64 getAmountOutTakeFeeFromOutToken(sint64& amountIn, sint64& reserveIn, sint64& reserveOut, uint32 fee, uint128& numerator, uint128& denominator, uint128& tmpRes) - { - if (amountIn >= MAX_AMOUNT) return -1; - - numerator = uint128(reserveOut) * uint128(amountIn); - denominator = uint128(reserveIn + amountIn); - - tmpRes = div(numerator, denominator); - if ((tmpRes.high != 0)|| (tmpRes.low > 0x7FFFFFFFFFFFFFFF)) - { - return -1; - } - else - { - return sint64(tmpRes.low); - } - } - - // (reserveIn + x) * (reserveOut - amountOut/(1 - fee)) = reserveIn * reserveOut - // x = (reserveIn * amountOut * 10000) / (reserveOut * (10000-fee) - amountOut * 10000) - inline static sint64 getAmountInTakeFeeFromOutToken(sint64& amountOut, sint64& reserveIn, sint64& reserveOut, uint32 fee, uint128& numerator, uint128& denominator, uint128& tmpRes) - { - if (amountOut >= MAX_AMOUNT) return -1; - - // Calculate full numerator to avoid premature truncation - numerator = uint128(reserveIn) * uint128(amountOut) * uint128(QSWAP_SWAP_FEE_BASE); - - // Check: reserveOut * (1-fee) must be greater than amountOut - // Scale reserveOut by (10000-fee) and amountOut by 10000 for comparison - // Use tmpRes and denominator temporarily for the comparison - tmpRes = uint128(reserveOut) * uint128(QSWAP_SWAP_FEE_BASE - fee); - denominator = uint128(amountOut) * uint128(QSWAP_SWAP_FEE_BASE); - - if (tmpRes <= denominator) - { - return -1; - } - - denominator = tmpRes - denominator; - - // Use floor + 1 to ensure user pays at least enough (protects LPs) - tmpRes = div(numerator, denominator) + uint128(1); - - if ((tmpRes.high != 0) || (tmpRes.low > 0x7FFFFFFFFFFFFFFF)) - { - return -1; - } - else - { - return sint64(tmpRes.low); - } - } - - PUBLIC_FUNCTION(Fees) - { - output.assetIssuanceFee = state.get().cachedIssuanceFee; - output.poolCreationFee = uint32(div(uint64(state.get().cachedIssuanceFee) * uint64(state.get().poolCreationFeeRate), uint64(QSWAP_FEE_BASE_100))); - output.transferFee = state.get().cachedTransferFee; - output.swapFee = state.get().swapFeeRate; - output.shareholderFee = state.get().shareholderFeeRate; - output.investRewardsFee = state.get().investRewardsFeeRate; - output.qxFee = state.get().qxFeeRate; - output.burnFee = state.get().burnFeeRate; - } - - struct GetPoolBasicState_locals - { - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - uint32 i0; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(GetPoolBasicState) - { - output.poolExists = 0; - output.totalLiquidity = -1; - output.reservedAssetAmount = -1; - output.reservedQuAmount = -1; - - // asset not issued - if (!qpi.isAssetIssued(input.assetIssuer, input.assetName)) - { - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = NULL_INDEX; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - if (locals.poolSlot == NULL_INDEX) - { - return; - } - - output.poolExists = 1; - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - output.reservedQuAmount = locals.poolBasicState.reservedQuAmount; - output.reservedAssetAmount = locals.poolBasicState.reservedAssetAmount; - output.totalLiquidity = locals.poolBasicState.totalLiquidity; - } - - struct GetLiquidityOf_locals - { - id poolID; - sint64 liqElementIndex; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(GetLiquidityOf) - { - output.liquidity = 0; - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.liqElementIndex = state.get().mLiquidities.headIndex(locals.poolID, 0); - - while (locals.liqElementIndex != NULL_INDEX) - { - if (state.get().mLiquidities.element(locals.liqElementIndex).entity == input.account) - { - output.liquidity = state.get().mLiquidities.element(locals.liqElementIndex).liquidity; - return; - } - locals.liqElementIndex = state.get().mLiquidities.nextElementIndex(locals.liqElementIndex); - } - } - - struct QuoteExactQuInput_locals - { - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - - uint32 i0; - uint128 i1, i2, i3, i4; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(QuoteExactQuInput) - { - output.assetAmountOut = -1; - - if (input.quAmountIn <= 0) - { - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - // no available slot for new pool - if (locals.poolSlot == -1) - { - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // no liquidity in the pool - if (locals.poolBasicState.totalLiquidity == 0) - { - return; - } - - output.assetAmountOut = getAmountOutTakeFeeFromInToken( - input.quAmountIn, - locals.poolBasicState.reservedQuAmount, - locals.poolBasicState.reservedAssetAmount, - state.get().swapFeeRate, - locals.i1, - locals.i2, - locals.i3, - locals.i4 - ); - } - - struct QuoteExactQuOutput_locals - { - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - - uint32 i0; - uint128 i1, i2, i3; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(QuoteExactQuOutput) - { - output.assetAmountIn = -1; - - if (input.quAmountOut <= 0) - { - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - // no available slot for new pool - if (locals.poolSlot == -1) - { - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // no liquidity in the pool - if (locals.poolBasicState.totalLiquidity == 0) - { - return; - } - - if (input.quAmountOut >= locals.poolBasicState.reservedQuAmount) - { - return; - } - - output.assetAmountIn = getAmountInTakeFeeFromOutToken( - input.quAmountOut, - locals.poolBasicState.reservedAssetAmount, - locals.poolBasicState.reservedQuAmount, - state.get().swapFeeRate, - locals.i1, - locals.i2, - locals.i3 - ); - } - - struct QuoteExactAssetInput_locals - { - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - sint64 quAmountOutWithFee; - - uint32 i0; - uint128 i1, i2, i3; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(QuoteExactAssetInput) - { - output.quAmountOut = -1; - - if (input.assetAmountIn <= 0) - { - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - // no available slot for new pool - if (locals.poolSlot == -1) - { - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // no liquidity in the pool - if (locals.poolBasicState.totalLiquidity == 0) - { - return; - } - - locals.quAmountOutWithFee = getAmountOutTakeFeeFromOutToken( - input.assetAmountIn, - locals.poolBasicState.reservedAssetAmount, - locals.poolBasicState.reservedQuAmount, - state.get().swapFeeRate, - locals.i1, - locals.i2, - locals.i3 - ); - - // above call overflow - if (locals.quAmountOutWithFee == -1) - { - return; - } - - // amount * (1-fee), no overflow risk - output.quAmountOut = sint64(div( - uint128(locals.quAmountOutWithFee) * uint128(QSWAP_SWAP_FEE_BASE - state.get().swapFeeRate), - uint128(QSWAP_SWAP_FEE_BASE) - ).low); - } - - struct QuoteExactAssetOutput_locals - { - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - - uint32 i0; - uint128 i1, i2, i3; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(QuoteExactAssetOutput) - { - output.quAmountIn = -1; - - if (input.assetAmountOut <= 0) - { - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - // no available slot for new pool - if (locals.poolSlot == -1) - { - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // no liquidity in the pool - if (locals.poolBasicState.totalLiquidity == 0) - { - return; - } - - if (input.assetAmountOut >= locals.poolBasicState.reservedAssetAmount) - { - return; - } - - output.quAmountIn = getAmountInTakeFeeFromInToken( - input.assetAmountOut, - locals.poolBasicState.reservedQuAmount, - locals.poolBasicState.reservedAssetAmount, - state.get().swapFeeRate, - locals.i1, - locals.i2, - locals.i3 - ); - } - - PUBLIC_FUNCTION(InvestRewardsInfo) - { - output.investRewardsFee = state.get().investRewardsFeeRate; - output.investRewardsId = state.get().investRewardsId; - } - -// -// procedure -// - PUBLIC_PROCEDURE(IssueAsset) - { - output.issuedNumberOfShares = 0; - if ((qpi.invocationReward() < state.get().cachedIssuanceFee)) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - - // check the validity of input - if ((input.numberOfShares <= 0) || (input.numberOfDecimalPlaces < 0)) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - - // asset already issued - if (qpi.isAssetIssued(qpi.invocator(), input.assetName)) - { - if (qpi.invocationReward() > 0 ) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - - output.issuedNumberOfShares = qpi.issueAsset( - input.assetName, - qpi.invocator(), - input.numberOfDecimalPlaces, - input.numberOfShares, - input.unitOfMeasurement - ); - - if (output.issuedNumberOfShares == 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - else - { - if (qpi.invocationReward() > state.get().cachedIssuanceFee) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.get().cachedIssuanceFee); - } - state.mut().shareholderEarnedFee += state.get().cachedIssuanceFee; - } - } - - struct CreatePool_locals - { - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - uint32 poolCreationFee; - - uint32 i0, i1; - }; - - // create uniswap like pool - // TODO: reject if there is no shares avaliabe shares in current contract, e.g. asset is issue in contract qx - PUBLIC_PROCEDURE_WITH_LOCALS(CreatePool) - { - output.success = false; - - locals.poolCreationFee = uint32(div(uint64(state.get().cachedIssuanceFee) * uint64(state.get().poolCreationFeeRate), uint64(QSWAP_FEE_BASE_100))); - - // fee check - if (qpi.invocationReward() < locals.poolCreationFee) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - - // asset no exist - if (!qpi.isAssetIssued(input.assetIssuer, input.assetName)) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - // check if pool already exist - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL ; locals.i0 ++ ) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - } - - // find an vacant pool slot - locals.poolSlot = -1; - for (locals.i1 = 0; locals.i1 < QSWAP_MAX_POOL; locals.i1 ++) - { - if (state.get().mPoolBasicStates.get(locals.i1).poolID == id(0,0,0,0)) - { - locals.poolSlot = locals.i1; - break; - } - } - - // no available slot for new pool - if (locals.poolSlot == -1) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.poolBasicState.poolID = locals.poolID; - locals.poolBasicState.reservedAssetAmount = 0; - locals.poolBasicState.reservedQuAmount = 0; - locals.poolBasicState.totalLiquidity = 0; - - state.mut().mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); - - if(qpi.invocationReward() > locals.poolCreationFee) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.poolCreationFee ); - } - state.mut().shareholderEarnedFee += locals.poolCreationFee; - - output.success = true; - } - - - struct AddLiquidity_locals - { - AddLiquidityMessage addLiquidityMessage; - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - LiquidityInfo tmpLiquidity; - - sint64 userLiquidityElementIndex; - sint64 quAmountDesired; - - sint64 quTransferAmount; - sint64 assetTransferAmount; - sint64 quOptimalAmount; - sint64 assetOptimalAmount; - sint64 increaseLiquidity; - sint64 reservedAssetAmountBefore; - sint64 reservedAssetAmountAfter; - - uint128 tmpIncLiq0; - uint128 tmpIncLiq1; - - uint32 i0; - uint128 i1, i2, i3; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(AddLiquidity) - { - output.userIncreaseLiquidity = 0; - output.assetAmount = 0; - output.quAmount = 0; - - // add liquidity must stake both qu and asset - if (qpi.invocationReward() <= 0) - { - return; - } - - locals.quAmountDesired = qpi.invocationReward(); - - // check the vadility of input params - if ((input.assetAmountDesired <= 0) || - (input.quAmountMin < 0) || - (input.assetAmountMin < 0)) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - // check the pool existance - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - if (locals.poolSlot == -1) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // check if pool state meet the input condition before desposit - // and confirm the final qu and asset amount to stake - if (locals.poolBasicState.totalLiquidity == 0) - { - locals.quTransferAmount = locals.quAmountDesired; - locals.assetTransferAmount = input.assetAmountDesired; - } - else - { - locals.assetOptimalAmount = quoteEquivalentAmountB( - locals.quAmountDesired, - locals.poolBasicState.reservedQuAmount, - locals.poolBasicState.reservedAssetAmount, - locals.i1 - ); - // overflow - if (locals.assetOptimalAmount == -1) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return ; - } - - if (locals.assetOptimalAmount <= input.assetAmountDesired ) - { - if (locals.assetOptimalAmount < input.assetAmountMin) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return ; - } - locals.quTransferAmount = locals.quAmountDesired; - locals.assetTransferAmount = locals.assetOptimalAmount; - } - else - { - locals.quOptimalAmount = quoteEquivalentAmountB( - input.assetAmountDesired, - locals.poolBasicState.reservedAssetAmount, - locals.poolBasicState.reservedQuAmount, - locals.i1 - ); - // overflow - if (locals.quOptimalAmount == -1) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return ; - } - if (locals.quOptimalAmount > locals.quAmountDesired) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return ; - } - if (locals.quOptimalAmount < input.quAmountMin) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return ; - } - locals.quTransferAmount = locals.quOptimalAmount; - locals.assetTransferAmount = input.assetAmountDesired; - } - } - - // check if the qu is enough - if (qpi.invocationReward() < locals.quTransferAmount) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // check if the asset is enough - if (qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - SELF_INDEX, - SELF_INDEX - ) < locals.assetTransferAmount) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // for pool's initial mint - if (locals.poolBasicState.totalLiquidity == 0) - { - locals.increaseLiquidity = sqrt(locals.quTransferAmount, locals.assetTransferAmount, locals.i1, locals.i2, locals.i3); - - if (locals.increaseLiquidity < QSWAP_MIN_LIQUIDITY ) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.reservedAssetAmountBefore = qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - SELF, - SELF, - SELF_INDEX, - SELF_INDEX - ); - qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - locals.assetTransferAmount, - SELF - ); - locals.reservedAssetAmountAfter = qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - SELF, - SELF, - SELF_INDEX, - SELF_INDEX - ); - - if (locals.reservedAssetAmountAfter - locals.reservedAssetAmountBefore < locals.assetTransferAmount) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // permanently lock the first MINIMUM_LIQUIDITY tokens - locals.tmpLiquidity.entity = SELF; - locals.tmpLiquidity.liquidity = QSWAP_MIN_LIQUIDITY; - state.mut().mLiquidities.add(locals.poolID, locals.tmpLiquidity, 0); - - locals.tmpLiquidity.entity = qpi.invocator(); - locals.tmpLiquidity.liquidity = locals.increaseLiquidity - QSWAP_MIN_LIQUIDITY; - state.mut().mLiquidities.add(locals.poolID, locals.tmpLiquidity, 0); - - output.quAmount = locals.quTransferAmount; - output.assetAmount = locals.assetTransferAmount; - output.userIncreaseLiquidity = locals.increaseLiquidity - QSWAP_MIN_LIQUIDITY; - } - else - { - locals.tmpIncLiq0 = div( - uint128(locals.quTransferAmount) * uint128(locals.poolBasicState.totalLiquidity), - uint128(locals.poolBasicState.reservedQuAmount) - ); - if (locals.tmpIncLiq0.high != 0 || locals.tmpIncLiq0.low > 0x7FFFFFFFFFFFFFFF) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - locals.tmpIncLiq1 = div( - uint128(locals.assetTransferAmount) * uint128(locals.poolBasicState.totalLiquidity), - uint128(locals.poolBasicState.reservedAssetAmount) - ); - if (locals.tmpIncLiq1.high != 0 || locals.tmpIncLiq1.low > 0x7FFFFFFFFFFFFFFF) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // increaseLiquity = min( - // quTransferAmount * totalLiquity / reserveQuAmount, - // assetTransferAmount * totalLiquity / reserveAssetAmount - // ); - locals.increaseLiquidity = min(sint64(locals.tmpIncLiq0.low), sint64(locals.tmpIncLiq1.low)); - - // maybe too little input - if (locals.increaseLiquidity == 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // find user liquidity index - locals.userLiquidityElementIndex = state.get().mLiquidities.headIndex(locals.poolID, 0); - while (locals.userLiquidityElementIndex != NULL_INDEX) - { - if(state.get().mLiquidities.element(locals.userLiquidityElementIndex).entity == qpi.invocator()) - { - break; - } - - locals.userLiquidityElementIndex = state.get().mLiquidities.nextElementIndex(locals.userLiquidityElementIndex); - } - - // no more space for new liquidity item - if ((locals.userLiquidityElementIndex == NULL_INDEX) && ( state.get().mLiquidities.population() == state.get().mLiquidities.capacity())) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // transfer the asset from invocator to contract - locals.reservedAssetAmountBefore = qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - SELF, - SELF, - SELF_INDEX, - SELF_INDEX - ); - qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - locals.assetTransferAmount, - SELF - ); - locals.reservedAssetAmountAfter = qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - SELF, - SELF, - SELF_INDEX, - SELF_INDEX - ); - - // only trust the amount in the contract - if (locals.reservedAssetAmountAfter - locals.reservedAssetAmountBefore < locals.assetTransferAmount) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - if (locals.userLiquidityElementIndex == NULL_INDEX) - { - locals.tmpLiquidity.entity = qpi.invocator(); - locals.tmpLiquidity.liquidity = locals.increaseLiquidity; - state.mut().mLiquidities.add(locals.poolID, locals.tmpLiquidity, 0); - } - else - { - locals.tmpLiquidity = state.get().mLiquidities.element(locals.userLiquidityElementIndex); - locals.tmpLiquidity.liquidity += locals.increaseLiquidity; - state.mut().mLiquidities.replace(locals.userLiquidityElementIndex, locals.tmpLiquidity); - } - - output.quAmount = locals.quTransferAmount; - output.assetAmount = locals.assetTransferAmount; - output.userIncreaseLiquidity = locals.increaseLiquidity; - } - - locals.poolBasicState.reservedQuAmount += locals.quTransferAmount; - locals.poolBasicState.reservedAssetAmount += locals.assetTransferAmount; - locals.poolBasicState.totalLiquidity += locals.increaseLiquidity; - - state.mut().mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); - - // Log AddLiquidity procedure - locals.addLiquidityMessage._contractIndex = SELF_INDEX; - locals.addLiquidityMessage._type = QSWAPAddLiquidity; - locals.addLiquidityMessage.assetIssuer = input.assetIssuer; - locals.addLiquidityMessage.assetName = input.assetName; - locals.addLiquidityMessage.userIncreaseLiquidity = output.userIncreaseLiquidity; - locals.addLiquidityMessage.quAmount = output.quAmount; - locals.addLiquidityMessage.assetAmount = output.assetAmount; - LOG_INFO(locals.addLiquidityMessage); - - if (qpi.invocationReward() > locals.quTransferAmount) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.quTransferAmount); - } - } - - struct RemoveLiquidity_locals - { - RemoveLiquidityMessage removeLiquidityMessage; - id poolID; - PoolBasicState poolBasicState; - sint64 userLiquidityElementIndex; - sint64 poolSlot; - LiquidityInfo userLiquidity; - sint64 burnQuAmount; - sint64 burnAssetAmount; - - uint32 i0; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(RemoveLiquidity) - { - output.quAmount = 0; - output.assetAmount = 0; - - if (qpi.invocationReward() > 0 ) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - - // check the vadility of input params - if (input.quAmountMin < 0 || input.assetAmountMin < 0 || input.burnLiquidity <= 0) - { - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - // get the pool's basic state - locals.poolSlot = -1; - - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - // the pool does not exsit - if (locals.poolSlot == -1) - { - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - locals.userLiquidityElementIndex = state.get().mLiquidities.headIndex(locals.poolID, 0); - while (locals.userLiquidityElementIndex != NULL_INDEX) - { - if(state.get().mLiquidities.element(locals.userLiquidityElementIndex).entity == qpi.invocator()) - { - break; - } - - locals.userLiquidityElementIndex = state.get().mLiquidities.nextElementIndex(locals.userLiquidityElementIndex); - } - - if (locals.userLiquidityElementIndex == NULL_INDEX) - { - return; - } - - locals.userLiquidity = state.get().mLiquidities.element(locals.userLiquidityElementIndex); - - // not enough liquidity for burning - if (locals.userLiquidity.liquidity < input.burnLiquidity ) - { - return; - } - - if (locals.poolBasicState.totalLiquidity < input.burnLiquidity ) - { - return; - } - - // since burnLiquidity < totalLiquidity, so there will be no overflow risk - locals.burnQuAmount = sint64(div( - uint128(input.burnLiquidity) * uint128(locals.poolBasicState.reservedQuAmount), - uint128(locals.poolBasicState.totalLiquidity) - ).low); - - // since burnLiquidity < totalLiquidity, so there will be no overflow risk - locals.burnAssetAmount = sint64(div( - uint128(input.burnLiquidity) * uint128(locals.poolBasicState.reservedAssetAmount), - uint128(locals.poolBasicState.totalLiquidity) - ).low); - - - if ((locals.burnQuAmount < input.quAmountMin) || (locals.burnAssetAmount < input.assetAmountMin)) - { - return; - } - - // return qu and asset to invocator - qpi.transfer(qpi.invocator(), locals.burnQuAmount); - qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - SELF, - SELF, - locals.burnAssetAmount, - qpi.invocator() - ); - - output.quAmount = locals.burnQuAmount; - output.assetAmount = locals.burnAssetAmount; - - // modify invocator's liquidity info - locals.userLiquidity.liquidity -= input.burnLiquidity; - if (locals.userLiquidity.liquidity == 0) - { - state.mut().mLiquidities.remove(locals.userLiquidityElementIndex); - } - else - { - state.mut().mLiquidities.replace(locals.userLiquidityElementIndex, locals.userLiquidity); - } - - // modify the pool's liquidity info - locals.poolBasicState.totalLiquidity -= input.burnLiquidity; - locals.poolBasicState.reservedQuAmount -= locals.burnQuAmount; - locals.poolBasicState.reservedAssetAmount -= locals.burnAssetAmount; - - state.mut().mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); - - // Log RemoveLiquidity procedure - locals.removeLiquidityMessage._contractIndex = SELF_INDEX; - locals.removeLiquidityMessage._type = QSWAPRemoveLiquidity; - locals.removeLiquidityMessage.quAmount = output.quAmount; - locals.removeLiquidityMessage.assetAmount = output.assetAmount; - LOG_INFO(locals.removeLiquidityMessage); - } - - struct SwapExactQuForAsset_locals - { - SwapMessage swapMessage; - id poolID; - sint64 poolSlot; - sint64 quAmountIn; - PoolBasicState poolBasicState; - sint64 assetAmountOut; - - uint32 i0; - uint128 i1, i2, i3, i4; - uint128 swapFee; - uint128 feeToInvestRewards; - uint128 feeToShareholders; - - uint128 feeToQx; - uint128 feeToBurn; - - sint64 totalFee; - }; - - // given an input qu amountIn, only execute swap in case (amountOut >= amountOutMin) - // https://docs.uniswap.org/contracts/v2/reference/smart-contracts/router-02#swapexacttokensfortokens - PUBLIC_PROCEDURE_WITH_LOCALS(SwapExactQuForAsset) - { - output.assetAmountOut = 0; - - // require input qu > 0 - if (qpi.invocationReward() <= 0) - { - return; - } - - if (input.assetAmountOutMin < 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.quAmountIn = qpi.invocationReward(); - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - if (locals.poolSlot == -1) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // check the liquidity validity - if (locals.poolBasicState.totalLiquidity == 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.assetAmountOut = getAmountOutTakeFeeFromInToken( - locals.quAmountIn, - locals.poolBasicState.reservedQuAmount, - locals.poolBasicState.reservedAssetAmount, - state.get().swapFeeRate, - locals.i1, - locals.i2, - locals.i3, - locals.i4 - ); - - // overflow - if (locals.assetAmountOut == -1) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // not meet user's amountOut requirement - if (locals.assetAmountOut < input.assetAmountOutMin) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // swapFee = quAmountIn * 0.3% (swapFeeRate/10000) - // swapFee distribution: 27% shareholders, 5% QX, 3% invest&rewards, 1% burn, 64% LP - locals.swapFee = div(uint128(locals.quAmountIn) * uint128(state.get().swapFeeRate), uint128(QSWAP_SWAP_FEE_BASE)); - if (locals.swapFee == uint128_t(0)) - { - locals.swapFee = QSWAP_FEE_BASE_100; - } - locals.feeToShareholders = div(locals.swapFee * uint128(state.get().shareholderFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToQx = div(locals.swapFee * uint128(state.get().qxFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToInvestRewards = div(locals.swapFee * uint128(state.get().investRewardsFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToBurn = div(locals.swapFee * uint128(state.get().burnFeeRate), uint128(QSWAP_FEE_BASE_100)); - - locals.totalFee = sint64(locals.feeToShareholders.low) + sint64(locals.feeToQx.low) + sint64(locals.feeToInvestRewards.low) + sint64(locals.feeToBurn.low); - - // Overflow protection: ensure all fees fit in uint64 - if (locals.feeToShareholders.high != 0 || locals.feeToQx.high != 0 - || locals.feeToInvestRewards.high != 0 || locals.feeToBurn.high != 0 - || locals.quAmountIn < locals.totalFee) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // transfer the asset from pool to qpi.invocator() - output.assetAmountOut = qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - SELF, - SELF, - locals.assetAmountOut, - qpi.invocator() - ) < 0 ? 0: locals.assetAmountOut; - - // in case asset transfer failed - if (output.assetAmountOut == 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // update fee state after successful transfer - state.mut().shareholderEarnedFee += locals.feeToShareholders.low; - state.mut().qxEarnedFee += locals.feeToQx.low; - state.mut().investRewardsEarnedFee += locals.feeToInvestRewards.low; - state.mut().burnEarnedFee += locals.feeToBurn.low; - - locals.poolBasicState.reservedQuAmount += locals.quAmountIn - locals.totalFee; - locals.poolBasicState.reservedAssetAmount -= locals.assetAmountOut; - state.mut().mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); - - // Log SwapExactQuForAsset procedure - locals.swapMessage._contractIndex = SELF_INDEX; - locals.swapMessage._type = QSWAPSwapExactQuForAsset; - locals.swapMessage.assetIssuer = input.assetIssuer; - locals.swapMessage.assetName = input.assetName; - locals.swapMessage.assetAmountIn = locals.quAmountIn; - locals.swapMessage.assetAmountOut = output.assetAmountOut; - LOG_INFO(locals.swapMessage); - } - - struct SwapQuForExactAsset_locals - { - SwapMessage swapMessage; - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - sint64 quAmountIn; - sint64 transferredAssetAmount; - - uint32 i0; - uint128 i1, i2, i3; - uint128 swapFee; - uint128 feeToInvestRewards; - uint128 feeToShareholders; - uint128 feeToQx; - uint128 feeToBurn; - - sint64 totalFee; - }; - - // https://docs.uniswap.org/contracts/v2/reference/smart-contracts/router-02#swaptokensforexacttokens - PUBLIC_PROCEDURE_WITH_LOCALS(SwapQuForExactAsset) - { - output.quAmountIn = 0; - - // require input qu amount > 0 - if (qpi.invocationReward() <= 0) - { - return; - } - - // check input param validity - if (input.assetAmountOut <= 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - if (locals.poolSlot == -1) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // check if there is liquidity in the poool - if (locals.poolBasicState.totalLiquidity == 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // check if reserved asset is enough - if (input.assetAmountOut >= locals.poolBasicState.reservedAssetAmount) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.quAmountIn = getAmountInTakeFeeFromInToken( - input.assetAmountOut, - locals.poolBasicState.reservedQuAmount, - locals.poolBasicState.reservedAssetAmount, - state.get().swapFeeRate, - locals.i1, - locals.i2, - locals.i3 - ); - - // above call overflow - if (locals.quAmountIn == -1 || locals.quAmountIn > qpi.invocationReward()) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // swapFee = quAmountIn * 0.3% (swapFeeRate/10000) - // swapFee distribution: 27% shareholders, 5% QX, 3% invest&rewards, 1% burn, 64% LP - locals.swapFee = div(uint128(locals.quAmountIn) * uint128(state.get().swapFeeRate), uint128(QSWAP_SWAP_FEE_BASE)); - if (locals.swapFee == uint128_t(0)) - { - locals.swapFee = QSWAP_FEE_BASE_100; - } - locals.feeToShareholders = div(locals.swapFee * uint128(state.get().shareholderFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToQx = div(locals.swapFee * uint128(state.get().qxFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToInvestRewards = div(locals.swapFee * uint128(state.get().investRewardsFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToBurn = div(locals.swapFee * uint128(state.get().burnFeeRate), uint128(QSWAP_FEE_BASE_100)); - - locals.totalFee = sint64(locals.feeToShareholders.low) + sint64(locals.feeToQx.low) + sint64(locals.feeToInvestRewards.low) + sint64(locals.feeToBurn.low); - if (locals.quAmountIn < locals.totalFee) - { - qpi.transfer(qpi.invocator(), locals.quAmountIn); - return; - } - - // Overflow protection: ensure all fees fit in uint64 - if (locals.feeToShareholders.high != 0 || locals.feeToQx.high != 0 || - locals.feeToInvestRewards.high != 0 || locals.feeToBurn.high != 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // transfer the asset from pool to qpi.invocator() - locals.transferredAssetAmount = qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - SELF, - SELF, - input.assetAmountOut, - qpi.invocator() - ) < 0 ? 0: input.assetAmountOut; - - // asset transfer failed - if (locals.transferredAssetAmount == 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - output.quAmountIn = locals.quAmountIn; - if (qpi.invocationReward() > locals.quAmountIn) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.quAmountIn); - } - - // update fee state after successful transfer - state.mut().shareholderEarnedFee += locals.feeToShareholders.low; - state.mut().qxEarnedFee += locals.feeToQx.low; - state.mut().investRewardsEarnedFee += locals.feeToInvestRewards.low; - state.mut().burnEarnedFee += locals.feeToBurn.low; - - locals.poolBasicState.reservedQuAmount += locals.quAmountIn - locals.totalFee; - locals.poolBasicState.reservedAssetAmount -= input.assetAmountOut; - state.mut().mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); - - // Log SwapQuForExactAsset procedure - locals.swapMessage._contractIndex = SELF_INDEX; - locals.swapMessage._type = QSWAPSwapQuForExactAsset; - locals.swapMessage.assetIssuer = input.assetIssuer; - locals.swapMessage.assetName = input.assetName; - locals.swapMessage.assetAmountIn = output.quAmountIn; - locals.swapMessage.assetAmountOut = input.assetAmountOut; - LOG_INFO(locals.swapMessage); - } - - struct SwapExactAssetForQu_locals - { - SwapMessage swapMessage; - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - sint64 quAmountOut; - sint64 quAmountOutWithFee; - sint64 transferredAssetAmountBefore; - sint64 transferredAssetAmountAfter; - - uint32 i0; - uint128 i1, i2, i3; - uint128 swapFee; - uint128 feeToInvestRewards; - uint128 feeToShareholders; - uint128 feeToQx; - uint128 feeToBurn; - - sint64 totalFee; - }; - - // given an amount of asset swap in, only execute swaping if quAmountOut >= input.amountOutMin - PUBLIC_PROCEDURE_WITH_LOCALS(SwapExactAssetForQu) - { - output.quAmountOut = 0; - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - - // check input param validity - if ((input.assetAmountIn <= 0 )||(input.quAmountOutMin < 0)) - { - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - if (locals.poolSlot == -1) - { - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // check the liquidity validity - if (locals.poolBasicState.totalLiquidity == 0) - { - return; - } - - // invocator's asset not enough - if (qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - SELF_INDEX, - SELF_INDEX - ) < input.assetAmountIn ) - { - return; - } - - locals.quAmountOutWithFee = getAmountOutTakeFeeFromOutToken( - input.assetAmountIn, - locals.poolBasicState.reservedAssetAmount, - locals.poolBasicState.reservedQuAmount, - state.get().swapFeeRate, - locals.i1, - locals.i2, - locals.i3 - ); - - // above call overflow - if (locals.quAmountOutWithFee == -1) - { - return; - } - - // no overflow risk - // locals.quAmountOutWithFee * (QSWAP_SWAP_FEE_BASE - state.swapFeeRate) / QSWAP_SWAP_FEE_BASE - locals.quAmountOut = sint64(div( - uint128(locals.quAmountOutWithFee) * uint128(QSWAP_SWAP_FEE_BASE - state.get().swapFeeRate), - uint128(QSWAP_SWAP_FEE_BASE) - ).low); - - // not meet user min amountOut requirement - if (locals.quAmountOut < input.quAmountOutMin) - { - return; - } - - // swapFee = quAmountOutWithFee * 0.3% (swapFeeRate/10000) - // swapFee distribution: 27% shareholders, 5% QX, 3% invest&rewards, 1% burn, 64% LP - locals.swapFee = div(uint128(locals.quAmountOutWithFee) * uint128(state.get().swapFeeRate), uint128(QSWAP_SWAP_FEE_BASE)); - if (locals.swapFee == uint128_t(0)) - { - locals.swapFee = QSWAP_FEE_BASE_100; - } - locals.feeToShareholders = div(locals.swapFee * uint128(state.get().shareholderFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToQx = div(locals.swapFee * uint128(state.get().qxFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToInvestRewards = div(locals.swapFee * uint128(state.get().investRewardsFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToBurn = div(locals.swapFee * uint128(state.get().burnFeeRate), uint128(QSWAP_FEE_BASE_100)); - - // Overflow protection: ensure all fees fit in uint64 - if (locals.feeToShareholders.high != 0 || locals.feeToQx.high != 0 || - locals.feeToInvestRewards.high != 0 || locals.feeToBurn.high != 0) - { - return; - } - - // transfer assets from user to pool - locals.transferredAssetAmountBefore = qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - SELF, - SELF, - SELF_INDEX, - SELF_INDEX - ); - qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - input.assetAmountIn, - SELF - ); - locals.transferredAssetAmountAfter = qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - SELF, - SELF, - SELF_INDEX, - SELF_INDEX - ); - - // pool does not receive enough asset, rollback any received shares - if (locals.transferredAssetAmountAfter - locals.transferredAssetAmountBefore < input.assetAmountIn) - { - // return any shares that were transferred - if (locals.transferredAssetAmountAfter > locals.transferredAssetAmountBefore) - { - qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - SELF, - SELF, - locals.transferredAssetAmountAfter - locals.transferredAssetAmountBefore, - qpi.invocator() - ); - } - return; - } - - qpi.transfer(qpi.invocator(), locals.quAmountOut); - output.quAmountOut = locals.quAmountOut; - - // update fee state after successful transfers - state.mut().shareholderEarnedFee += locals.feeToShareholders.low; - state.mut().qxEarnedFee += locals.feeToQx.low; - state.mut().investRewardsEarnedFee += locals.feeToInvestRewards.low; - state.mut().burnEarnedFee += locals.feeToBurn.low; - - // update pool states - locals.poolBasicState.reservedAssetAmount += input.assetAmountIn; - locals.totalFee = locals.quAmountOut + sint64(locals.feeToShareholders.low) + sint64(locals.feeToQx.low) + sint64(locals.feeToInvestRewards.low) + sint64(locals.feeToBurn.low); - if (locals.poolBasicState.reservedQuAmount < locals.totalFee) - { - locals.poolBasicState.reservedQuAmount = 0; - } - else - { - locals.poolBasicState.reservedQuAmount -= locals.totalFee; - } - state.mut().mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); - - // Log SwapExactAssetForQu procedure - locals.swapMessage._contractIndex = SELF_INDEX; - locals.swapMessage._type = QSWAPSwapExactAssetForQu; - locals.swapMessage.assetIssuer = input.assetIssuer; - locals.swapMessage.assetName = input.assetName; - locals.swapMessage.assetAmountIn = input.assetAmountIn; - locals.swapMessage.assetAmountOut = output.quAmountOut; - LOG_INFO(locals.swapMessage); - } - - struct SwapAssetForExactQu_locals - { - SwapMessage swapMessage; - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - sint64 assetAmountIn; - sint64 transferredAssetAmountBefore; - sint64 transferredAssetAmountAfter; - - uint32 i0; - uint128 i1, i2, i3; - uint128 swapFee; - uint128 feeToInvestRewards; - uint128 feeToShareholders; - uint128 feeToQx; - uint128 feeToBurn; - - sint64 totalFee; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(SwapAssetForExactQu) - { - output.assetAmountIn = 0; - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - - if ((input.assetAmountInMax <= 0 )||(input.quAmountOut <= 0)) - { - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - if (locals.poolSlot == -1) - { - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // check the liquidity validity - if (locals.poolBasicState.totalLiquidity == 0) - { - return; - } - - // pool does not hold enough asset - if (input.quAmountOut >= locals.poolBasicState.reservedQuAmount) - { - return; - } - - locals.assetAmountIn = getAmountInTakeFeeFromOutToken( - input.quAmountOut, - locals.poolBasicState.reservedAssetAmount, - locals.poolBasicState.reservedQuAmount, - state.get().swapFeeRate, - locals.i1, - locals.i2, - locals.i3 - ); - - // invalid input, assetAmountIn overflow - if (locals.assetAmountIn == -1) - { - return; - } - - // user does not hold enough asset - if (qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - SELF_INDEX, - SELF_INDEX - ) < locals.assetAmountIn ) - { - return; - } - - // not meet user amountIn reqiurement - if (locals.assetAmountIn > input.assetAmountInMax) - { - return; - } - - // swapFee = quAmountOut * 30/(10_000 - 30) - // swapFee distribution: 27% shareholders, 5% QX, 3% invest&rewards, 1% burn, 64% LP - locals.swapFee = div(uint128(input.quAmountOut) * uint128(state.get().swapFeeRate), uint128(QSWAP_SWAP_FEE_BASE - state.get().swapFeeRate)); - if (locals.swapFee == uint128_t(0)) - { - locals.swapFee = QSWAP_FEE_BASE_100; - } - locals.feeToShareholders = div(locals.swapFee * uint128(state.get().shareholderFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToQx = div(locals.swapFee * uint128(state.get().qxFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToInvestRewards = div(locals.swapFee * uint128(state.get().investRewardsFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToBurn = div(locals.swapFee * uint128(state.get().burnFeeRate), uint128(QSWAP_FEE_BASE_100)); - - // Overflow protection: ensure all fees fit in uint64 - if (locals.feeToShareholders.high != 0 || locals.feeToQx.high != 0 || - locals.feeToInvestRewards.high != 0 || locals.feeToBurn.high != 0) - { - return; - } - - locals.transferredAssetAmountBefore = qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - SELF, - SELF, - SELF_INDEX, - SELF_INDEX - ); - qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - locals.assetAmountIn, - SELF - ); - locals.transferredAssetAmountAfter = qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - SELF, - SELF, - SELF_INDEX, - SELF_INDEX - ); - - // pool does not receive enough asset, rollback any received shares - if (locals.transferredAssetAmountAfter - locals.transferredAssetAmountBefore < locals.assetAmountIn) - { - // return any shares that were transferred - if (locals.transferredAssetAmountAfter > locals.transferredAssetAmountBefore) - { - qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - SELF, - SELF, - locals.transferredAssetAmountAfter - locals.transferredAssetAmountBefore, - qpi.invocator() - ); - } - return; - } - - qpi.transfer(qpi.invocator(), input.quAmountOut); - output.assetAmountIn = locals.assetAmountIn; - - // update fee state after successful transfers - state.mut().shareholderEarnedFee += locals.feeToShareholders.low; - state.mut().qxEarnedFee += locals.feeToQx.low; - state.mut().investRewardsEarnedFee += locals.feeToInvestRewards.low; - state.mut().burnEarnedFee += locals.feeToBurn.low; - - // update pool states - locals.poolBasicState.reservedAssetAmount += locals.assetAmountIn; - locals.totalFee = input.quAmountOut + sint64(locals.feeToShareholders.low) + sint64(locals.feeToQx.low) + sint64(locals.feeToInvestRewards.low) + sint64(locals.feeToBurn.low); - if (locals.poolBasicState.reservedQuAmount < locals.totalFee) - { - locals.poolBasicState.reservedQuAmount = 0; - } - else - { - locals.poolBasicState.reservedQuAmount -= locals.totalFee; - } - state.mut().mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); - - // Log SwapAssetForExactQu procedure - locals.swapMessage._contractIndex = SELF_INDEX; - locals.swapMessage._type = QSWAPSwapAssetForExactQu; - locals.swapMessage.assetIssuer = input.assetIssuer; - locals.swapMessage.assetName = input.assetName; - locals.swapMessage.assetAmountIn = output.assetAmountIn; - locals.swapMessage.assetAmountOut = input.quAmountOut; - LOG_INFO(locals.swapMessage); - } - - PUBLIC_PROCEDURE(TransferShareOwnershipAndPossession) - { - output.transferredAmount = 0; - - if (qpi.invocationReward() < state.get().cachedTransferFee) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - if (input.amount <= 0) - { - if (qpi.invocationReward() > 0 ) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - - if (qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - SELF_INDEX, - SELF_INDEX - ) < input.amount) - { - - if (qpi.invocationReward() > 0 ) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - - output.transferredAmount = qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - input.amount, - input.newOwnerAndPossessor - ) < 0 ? 0 : input.amount; - - if (output.transferredAmount == 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - else - { - if (qpi.invocationReward() > state.get().cachedTransferFee) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.get().cachedTransferFee); - } - state.mut().shareholderEarnedFee += state.get().cachedTransferFee; - } - } - - PUBLIC_PROCEDURE(SetInvestRewardsInfo) - { - output.success = false; - if (qpi.invocator() != state.get().investRewardsId) - { - return; - } - - state.mut().investRewardsId = input.newInvestRewardsId; - output.success = true; - } - - struct TransferShareManagementRights_locals - { - sint64 result; - sint64 reward; - sint64 refundAmount; - sint64 requiredFee; - bit success; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(TransferShareManagementRights) - { - locals.reward = qpi.invocationReward(); - locals.refundAmount = locals.reward; - - output.transferredNumberOfShares = 0; - - locals.success = false; - - if (qpi.numberOfPossessedShares( - input.asset.assetName, - input.asset.issuer, - qpi.invocator(), - qpi.invocator(), - SELF_INDEX, - SELF_INDEX) >= input.numberOfShares) - { - locals.result = qpi.releaseShares( - input.asset, - qpi.invocator(), - qpi.invocator(), - input.numberOfShares, - input.newManagingContractIndex, - input.newManagingContractIndex, - locals.reward - ); - - if (locals.result != INVALID_AMOUNT && locals.result >= 0) - { - locals.success = true; - locals.refundAmount = locals.reward - locals.result; - } - } - - if (locals.success) - { - output.transferredNumberOfShares = input.numberOfShares; - } - - if (locals.refundAmount > 0) - { - qpi.transfer(qpi.invocator(), locals.refundAmount); - } - } - - REGISTER_USER_FUNCTIONS_AND_PROCEDURES() - { - // functions - REGISTER_USER_FUNCTION(Fees, 1); - REGISTER_USER_FUNCTION(GetPoolBasicState, 2); - REGISTER_USER_FUNCTION(GetLiquidityOf, 3); - REGISTER_USER_FUNCTION(QuoteExactQuInput, 4); - REGISTER_USER_FUNCTION(QuoteExactQuOutput, 5); - REGISTER_USER_FUNCTION(QuoteExactAssetInput, 6); - REGISTER_USER_FUNCTION(QuoteExactAssetOutput, 7); - REGISTER_USER_FUNCTION(InvestRewardsInfo, 8); - - // procedure - REGISTER_USER_PROCEDURE(IssueAsset, 1); - REGISTER_USER_PROCEDURE(TransferShareOwnershipAndPossession, 2); - REGISTER_USER_PROCEDURE(CreatePool, 3); - REGISTER_USER_PROCEDURE(AddLiquidity, 4); - REGISTER_USER_PROCEDURE(RemoveLiquidity, 5); - REGISTER_USER_PROCEDURE(SwapExactQuForAsset, 6); - REGISTER_USER_PROCEDURE(SwapQuForExactAsset, 7); - REGISTER_USER_PROCEDURE(SwapExactAssetForQu, 8); - REGISTER_USER_PROCEDURE(SwapAssetForExactQu, 9); - REGISTER_USER_PROCEDURE(SetInvestRewardsInfo, 10); - REGISTER_USER_PROCEDURE(TransferShareManagementRights, 11); - } - - INITIALIZE() - { - state.mut().swapFeeRate = 30; // 0.3%, must be less than 10000 - state.mut().poolCreationFeeRate = 20; // 20%, must be less than 100 - - // swapFee distribution: 27% shareholders, 5% QX, 3% invest&rewards, 1% burn, 64% LP providers - state.mut().shareholderFeeRate = 27; // 27% of swap fees to SC shareholders - state.mut().investRewardsFeeRate = 3; // 3% of swap fees to Invest & Rewards - state.mut().qxFeeRate = 5; // 5% of swap fees to QX - state.mut().burnFeeRate = 1; // 1% of swap fees burned - - ASSERT(state.get().swapFeeRate < QSWAP_SWAP_FEE_BASE); - ASSERT(state.get().shareholderFeeRate + state.get().investRewardsFeeRate + state.get().qxFeeRate + state.get().burnFeeRate <= 100); - // - state.mut().investRewardsId = ID(_V, _J, _G, _R, _U, _F, _W, _J, _C, _U, _S, _N, _H, _C, _Q, _J, _R, _W, _R, _R, _Y, _X, _A, _U, _E, _J, _F, _C, _V, _H, _Y, _P, _X, _W, _K, _T, _D, _L, _Y, _K, _U, _A, _C, _P, _V, _V, _Y, _B, _G, _O, _L, _V, _C, _J, _S, _F); - } - - struct BEGIN_EPOCH_locals - { - QX::Fees_input feesInput; - QX::Fees_output feesOutput; - }; - - BEGIN_EPOCH_WITH_LOCALS() - { - CALL_OTHER_CONTRACT_FUNCTION(QX, Fees, locals.feesInput, locals.feesOutput); - - if (interContractCallError == NoCallError) - { - state.mut().cachedIssuanceFee = locals.feesOutput.assetIssuanceFee; - state.mut().cachedTransferFee = locals.feesOutput.transferFee; - } - } - - struct END_TICK_locals - { - uint64 toDistribute; - uint64 toBurn; - uint64 dividendPerComputor; - sint64 transferredAmount; - FailedDistributionMessage logMsg; - }; - - END_TICK_WITH_LOCALS() - { - // Distribute Invest & Rewards fees - if (state.get().investRewardsEarnedFee > state.get().investRewardsDistributedAmount) - { - locals.toDistribute = state.get().investRewardsEarnedFee - state.get().investRewardsDistributedAmount; - locals.transferredAmount = qpi.transfer(state.get().investRewardsId, locals.toDistribute); - if (locals.transferredAmount < 0) - { - locals.logMsg._contractIndex = SELF_INDEX; - locals.logMsg._type = QSWAPFailedDistribution; - locals.logMsg.dst = state.get().investRewardsId; - locals.logMsg.amount = locals.toDistribute; - LOG_INFO(locals.logMsg); - } - else - state.mut().investRewardsDistributedAmount += locals.toDistribute; - } - - // Distribute QX fees as donation - if (state.get().qxEarnedFee > state.get().qxDistributedAmount) - { - locals.toDistribute = state.get().qxEarnedFee - state.get().qxDistributedAmount; - locals.transferredAmount = qpi.transfer(id(QX_CONTRACT_INDEX, 0, 0, 0), locals.toDistribute); - if (locals.transferredAmount < 0) - { - locals.logMsg._contractIndex = SELF_INDEX; - locals.logMsg._type = QSWAPFailedDistribution; - locals.logMsg.dst = id(QX_CONTRACT_INDEX, 0, 0, 0); - locals.logMsg.amount = locals.toDistribute; - LOG_INFO(locals.logMsg); - } - else - state.mut().qxDistributedAmount += locals.toDistribute; - } - - // Distribute shareholder fees (to IPO shareholders via dividends) - if (state.get().shareholderEarnedFee > state.get().shareholderDistributedAmount) - { - locals.dividendPerComputor = div((state.get().shareholderEarnedFee - state.get().shareholderDistributedAmount), 676ULL); - if (locals.dividendPerComputor > 0 && qpi.distributeDividends(locals.dividendPerComputor)) - { - state.mut().shareholderDistributedAmount += locals.dividendPerComputor * NUMBER_OF_COMPUTORS; - } - } - - // Burn fees (adds to contract execution fee reserve) - if (state.get().burnEarnedFee > state.get().burnedAmount) - { - locals.toBurn = state.get().burnEarnedFee - state.get().burnedAmount; - qpi.burn(locals.toBurn); - state.mut().burnedAmount += locals.toBurn; - } - } - - PRE_ACQUIRE_SHARES() - { - output.allowTransfer = true; - } -}; From 018b50c8c59419a99d9d950604a44bec8ec5975d Mon Sep 17 00:00:00 2001 From: feiyu Date: Tue, 1 Sep 2026 18:28:18 +0700 Subject: [PATCH 13/21] Read a cheat payload at guest offset zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit w_cheat treated a zero offset as "no payload", but offset 0 is an ordinary linear-memory address and contract state sits there — so every CC_PRINT of a state read reached the trace with no bytes. The length says whether there is a payload; the offset never did. Only a real node showed this. The simulator's import already keys on the length, and slicing a JS array at 0 is fine, so every engine test passed while the native path dropped exactly the reads worth printing. recordCheat also refuses to dereference a null payload now: an offset outside linear memory resolves to null, and the size alone was enough to send it to hex(). --- src/extensions/wasm/runtime/lhost_registry.h | 4 +++- src/extensions/wasm/runtime/trace.h | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/extensions/wasm/runtime/lhost_registry.h b/src/extensions/wasm/runtime/lhost_registry.h index 03af8200..ee4e83dd 100644 --- a/src/extensions/wasm/runtime/lhost_registry.h +++ b/src/extensions/wasm/runtime/lhost_registry.h @@ -263,7 +263,9 @@ static void w_logBytes(wasm_exec_env_t execEnv, uint32_t contractIndex, uint32_t static int64_t w_cheat(wasm_exec_env_t execEnv, uint32_t op, uint64_t a, uint64_t b, uint32_t ptrOffset, uint32_t len) { CallContext* callContext = activeCallContext(execEnv); - void* payload = ptrOffset ? nativeAddress(execEnv, ptrOffset) : nullptr; + // The length says whether there is a payload, not the offset: offset 0 is an ordinary address, and + // contract state lives there, so testing the offset drops exactly the reads worth printing. + void* payload = len ? nativeAddress(execEnv, ptrOffset) : nullptr; if (op == CHEAT_OP_PRINT) { diff --git a/src/extensions/wasm/runtime/trace.h b/src/extensions/wasm/runtime/trace.h index f503ce29..4049bd5b 100644 --- a/src/extensions/wasm/runtime/trace.h +++ b/src/extensions/wasm/runtime/trace.h @@ -239,8 +239,11 @@ static inline void recordCheat(TraceEntry* entry, unsigned int id, unsigned char return; } + // A guest offset outside linear memory resolves to null; record the size but never read from it. + const bool readable = bytes && size; + entry->cheats.push_back(CheatEntry{ - id, part, size, value, size ? hex(bytes, size) : std::string(), + id, part, size, value, readable ? hex(bytes, size) : std::string(), }); } From c2ee8b43140e1bce9cdd5b2e6aa32356b2312b75 Mon Sep 17 00:00:00 2001 From: baoLuck <91096117+baoLuck@users.noreply.github.com> Date: Tue, 1 Sep 2026 23:31:39 +0300 Subject: [PATCH 14/21] QIP return funds fix (#993) --- src/contracts/QIP.h | 12 ++++++++++-- test/contract_qip.cpp | 6 ++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/contracts/QIP.h b/src/contracts/QIP.h index bec11e1e..de4483d2 100644 --- a/src/contracts/QIP.h +++ b/src/contracts/QIP.h @@ -29,6 +29,7 @@ enum QIPLogInfo { QIP_invalidIssuer = 15, QIP_qxAskOrderFound = 16, QIP_icoAlreadyExists = 17, + QIP_returnFundsError = 18, }; struct QIPLogger @@ -715,9 +716,16 @@ struct QIP : public ContractBase } } - if (locals.log._type == QIP_success && locals.buyerInfo.isReturned) + if (locals.log._type == QIP_success) { - locals.log._type = QIPLogInfo::QIP_fundsAlreadyReturned; + if (qpi.epoch() <= locals.ico.startEpoch + 2) + { + locals.log._type = QIPLogInfo::QIP_returnFundsError; + } + else if (locals.buyerInfo.isReturned) + { + locals.log._type = QIPLogInfo::QIP_fundsAlreadyReturned; + } } if (locals.log._type != QIP_success) diff --git a/test/contract_qip.cpp b/test/contract_qip.cpp index 90b0a940..5cb8c0c8 100644 --- a/test/contract_qip.cpp +++ b/test/contract_qip.cpp @@ -1141,7 +1141,6 @@ TEST(ContractQIP, returnFunds) QIP.beginEpoch(); QIP::buyToken_output buyOutput2 = QIP.buyToken(buyer2, 0, buyAmount2, requiredReward2); EXPECT_EQ(buyOutput2.returnCode, QIPLogInfo::QIP_success); - //QIP.endEpoch(); EXPECT_EQ(numberOfPossessedShares(assetName, issuer, buyer, buyer, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), 0); EXPECT_EQ(numberOfPossessedShares(assetName, issuer, buyer2, buyer2, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), 0); @@ -1149,6 +1148,9 @@ TEST(ContractQIP, returnFunds) EXPECT_EQ(getBalance(QIP_CONTRACT_ID), 24171804); EXPECT_EQ(getBalance(QIP_testAddress2), 0); + QIP::returnFunds_output returnFundsOutput = QIP.returnFunds(buyer, 0); + EXPECT_EQ(returnFundsOutput.returnCode, QIPLogInfo::QIP_returnFundsError); + QIP.endEpoch(); // 1 EXPECT_EQ(numberOfPossessedShares(assetName, issuer, buyer, buyer, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), 792); // buyAmount / vestingPeriod @@ -1177,7 +1179,7 @@ TEST(ContractQIP, returnFunds) ++system.epoch; // 4 QIP.beginEpoch(); EXPECT_EQ(getBalance(buyer), 0); - QIP::returnFunds_output returnFundsOutput = QIP.returnFunds(buyer, 0); + returnFundsOutput = QIP.returnFunds(buyer, 0); EXPECT_EQ(returnFundsOutput.returnCode, QIPLogInfo::QIP_success); EXPECT_EQ(getBalance(buyer), 4059429); QIP.endEpoch(); From 88d80f5479e997d28f477a119207f1bb10c1a3c1 Mon Sep 17 00:00:00 2001 From: fnordspace Date: Tue, 1 Sep 2026 22:47:03 +0200 Subject: [PATCH 15/21] Pin commit with hash in contract-verify workflow --- .github/workflows/contract-verify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/contract-verify.yml b/.github/workflows/contract-verify.yml index 672ae28c..243a5468 100644 --- a/.github/workflows/contract-verify.yml +++ b/.github/workflows/contract-verify.yml @@ -45,6 +45,6 @@ jobs: echo "oi-filepaths=$files2" >> "$GITHUB_OUTPUT" - name: Contract verify action step id: verify - uses: qubic/contract-verify@main + uses: qubic/contract-verify@970ce102d56df53b68f1b8fa65b2dd445d5c9d81 with: filepaths: '${{ steps.filepaths.outputs.contract-filepaths }},${{ steps.filepaths.outputs.oi-filepaths }}' From a2a14a59a2b836dd50ae0f95e7181d7778504b81 Mon Sep 17 00:00:00 2001 From: fnordspace Date: Tue, 1 Sep 2026 22:52:00 +0200 Subject: [PATCH 16/21] Add missing version comment --- .github/workflows/contract-verify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/contract-verify.yml b/.github/workflows/contract-verify.yml index 243a5468..f40a9764 100644 --- a/.github/workflows/contract-verify.yml +++ b/.github/workflows/contract-verify.yml @@ -45,6 +45,6 @@ jobs: echo "oi-filepaths=$files2" >> "$GITHUB_OUTPUT" - name: Contract verify action step id: verify - uses: qubic/contract-verify@970ce102d56df53b68f1b8fa65b2dd445d5c9d81 + uses: qubic/contract-verify@970ce102d56df53b68f1b8fa65b2dd445d5c9d81 # main with: filepaths: '${{ steps.filepaths.outputs.contract-filepaths }},${{ steps.filepaths.outputs.oi-filepaths }}' From fc4c630a329a3a44e3bd1efa32a266cfc73ebdb6 Mon Sep 17 00:00:00 2001 From: feiyu Date: Wed, 2 Sep 2026 12:36:17 +0700 Subject: [PATCH 17/21] Check call kind and locals size in the inter-contract macros --- src/extensions/wasm/sdk/intercontract_calls.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/extensions/wasm/sdk/intercontract_calls.h b/src/extensions/wasm/sdk/intercontract_calls.h index fab80bb6..6d15ec00 100644 --- a/src/extensions/wasm/sdk/intercontract_calls.h +++ b/src/extensions/wasm/sdk/intercontract_calls.h @@ -13,10 +13,13 @@ int invokeProcedure(const void* callerContext, unsigned int calleeIndex, unsigne } // namespace Wasm::Sdk -// Calls remain restricted to lower-index contracts. +// Calls remain restricted to lower-index contracts. The entry-kind and locals-size checks match +// qpi_macros.h, so a mistake fails to compile here instead of returning a call error at run time. #undef CALL_OTHER_CONTRACT_FUNCTION_E #define CALL_OTHER_CONTRACT_FUNCTION_E(contractStateType, function, input, output, errorVar) \ static_assert(contractStateType::__contract_index < CONTRACT_INDEX, "lite: can only call a lower-index contract"); \ + static_assert(sizeof(contractStateType::function##_locals) <= MAX_SIZE_OF_CONTRACT_LOCALS, #function "_locals size too large"); \ + static_assert(contractStateType::__is_function_##function, "CALL_OTHER_CONTRACT_FUNCTION_E() cannot be used to invoke procedures."); \ QPI::InterContractCallError errorVar = (QPI::InterContractCallError)Wasm::Sdk::callFunction( \ &qpi, contractStateType::__contract_index, contractStateType##_##function##_inputType, \ &(input), sizeof(input), &(output), sizeof(output)) @@ -28,6 +31,8 @@ int invokeProcedure(const void* callerContext, unsigned int calleeIndex, unsigne #undef INVOKE_OTHER_CONTRACT_PROCEDURE_E #define INVOKE_OTHER_CONTRACT_PROCEDURE_E(contractStateType, procedure, input, output, invocationReward, errorVar) \ static_assert(contractStateType::__contract_index < CONTRACT_INDEX, "lite: can only call a lower-index contract"); \ + static_assert(sizeof(contractStateType::procedure##_locals) <= MAX_SIZE_OF_CONTRACT_LOCALS, #procedure "_locals size too large"); \ + static_assert(!contractStateType::__is_function_##procedure, "INVOKE_OTHER_CONTRACT_PROCEDURE_E() cannot be used to call functions."); \ QPI::InterContractCallError errorVar = (QPI::InterContractCallError)Wasm::Sdk::invokeProcedure( \ &qpi, contractStateType::__contract_index, contractStateType##_##procedure##_inputType, \ &(input), sizeof(input), &(output), sizeof(output), (invocationReward)) From 26c45918eaf2a779b4af3546a5b732c4213a41e7 Mon Sep 17 00:00:00 2001 From: cyber-pc <165458555+cyber-pc@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:37:14 +0700 Subject: [PATCH 18/21] Update ant colony document. --- doc/ant_colony_mining.md | 47 ++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/doc/ant_colony_mining.md b/doc/ant_colony_mining.md index 4a996840..5e694af4 100644 --- a/doc/ant_colony_mining.md +++ b/doc/ant_colony_mining.md @@ -166,8 +166,9 @@ identical for every identity**; per-identity variation enters only through the m **Child.** `computeScoreFromParent(parentLUT, publicKey, nonce, anchorTickDigest)`: 1. Inherit `parentLUT`. -2. `mutationSeed = K12(publicKey || nonce[3..31] || anchorTickDigest)` (`nonce[0..2]` zeroed) - - still keyed by the mining identity, so different identities walk differently from the shared root. +2. `mutationSeed = K12(publicKey || nonce || anchorTickDigest)` with `nonce[0..2]` zeroed in place + (the full 32-byte nonce is hashed, its first 3 bytes set to 0, not dropped) - still keyed by the + mining identity, so different identities walk differently from the shared root. 3. Walk `numberOfMutations = 100` steps. Each step rewrites `L` LUT entries. For the first `K` steps accept a worse-or-equal result (**explore**); after that accept only better-or-equal (**exploit**); one-step rollback on reject. Keep and return the **best** score seen. The best is seeded with the @@ -185,8 +186,9 @@ Score is an error count in `[0, 8088]`; lower is better. ### 2.4 Accept rules -A submission is accepted (`Valid` or `ValidNotStored`) only if **all** of these hold. The node checks -in this order; the first failure is the reject reason: +A submission is accepted (`Valid` or `ValidNotStored`) only if **all** of these hold. On failure the +reject reason names the rule the node found violated (the exact evaluation order is an internal +detail and can change): | Check | Reject reason if it fails | |-------|---------------------------| @@ -237,8 +239,8 @@ BroadcastMessage { // 96-byte envelope } // then the payload: AntSolutionBroadcastPayload { // 48 bytes - unsigned int parentTick; // ABSOLUTE tick of the parent node (0 with the index below = root) - unsigned int parentSolutionIndexInTick; + unsigned int parentTick; // ABSOLUTE tick of the parent node; the virtual root is (0, 0xFFFFFFFF) + unsigned int parentSolutionIndexInTick; // parent's index within its tick; 0xFFFFFFFF with parentTick 0 = root unsigned int anchorTick; // ABSOLUTE tick number unsigned int claimedScore; m256i nonce; // the 32-byte nonce from 2.2 @@ -286,6 +288,37 @@ becomes `sourcePublicKey` of the transaction. Workers hold no tree and post no o hand solutions to the pool's computor, which pre-validates them and risks its own deposit only on solutions it expects to be accepted and refunded. Fund the computor identity, not the workers. +### 2.6a Submitting a root (depth-1) solution + +A root solution starts a tree: its parent is the epoch's shared virtual root, which is derived rather +than stored, so you never fetch it. This is the first solution every identity submits, and it differs +from extending an existing node only in how the parent is named and scored. + +1. **Parent is the root.** Set `parentRef = (parentTick = 0, parentSolutionIndexInTick = 0xFFFFFFFF)`. + Both fields are load-bearing: `(0, 0xFFFFFFFF)` is the only value the node reads as root; a `0` + tick with any other index is treated as a normal parent, found nowhere, and rejected with + `RejectParentNotRegistered`. +2. **Derive the parent LUT yourself.** `deriveRootANN(spectrumDigest, epochPool)` from the epoch + context (section 2.3) - do **not** call `REQUEST_ANT_PARENT_ANN` for the root; it answers + `status = IS_ROOT` with no ANN payload precisely so you derive it locally. The root is identical + for every identity. +3. **Search.** Pick a canonical nonce (section 2.2), inherit the derived root LUT, run the walk, and + take the best score - exactly as for any parent. +4. **The only score gate is the threshold.** The root's record score is the worst possible value, so + the "strictly beats the parent" rule passes trivially; a root child is accepted on score iff its + score is `<=` the epoch threshold. The shared root scores far above the threshold, so a valid + start still requires real mutation - and since every identity starts from the same root score, + ranking reflects search effort alone. +5. **Anchor and submit.** Choose a non-empty anchor tick within `freshnessWindow` of the publish tick + (section 2.3), fill the payload with the root `parentRef` above, and hand it to your computor + (section 2.6, stage 1). The computor publishes it as the usual `AntColonyMiningSolutionTransaction`. + +On the node, a root submission is recognized by `parentRef.isRoot()`: the parent lookup returns a +null record (root is not a stored solution), the node derives the shared root itself to recompute +your `claimedScore`, and root children are de-duplicated per miner because the root is shared by all +identities. Once accepted, the node becomes a normal parent - extend it by copying its `selfTick` / +`selfSolutionIndexInTick` from the identity-tree query (section 2.7b) into a child's `parentRef`. + ### 2.7 Read queries Three request/response pairs. **Identity tree** and **parent ANN** are **operator-signed**; **epoch @@ -373,7 +406,7 @@ unsigned char status; // 0 = OK, 1 = NOT_FOUND, 2 = IS_ROOT (derive the unsigned char padding[3]; ``` -**Canonical ANN layout.** The same byte form is used everywhere ANN bytes leave the node: this response, the snapshot pool, and the epoch export. Under the current bpp9000 parameters it is 1'728 bytes = 64 rows of 27: +**Canonical ANN layout.** The same byte form is used everywhere ANN bytes leave the node: this response, the snapshot pool, and the epoch export. Under the current bpp9000 parameters it is 1728 bytes = 64 rows of 27: ``` row k, k = 0..45 LUT of neuron updatedNeuronIndices[k]: the k-th NON-INPUT neuron in From ad5970b9b7538b8ec87873385780e4a0314d66a9 Mon Sep 17 00:00:00 2001 From: feiyu Date: Wed, 2 Sep 2026 15:01:13 +0700 Subject: [PATCH 19/21] disable ant sidecar process by default --- src/extensions/supervisor_shim.h | 2 +- src/qubic.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extensions/supervisor_shim.h b/src/extensions/supervisor_shim.h index 51950105..b22c27a2 100644 --- a/src/extensions/supervisor_shim.h +++ b/src/extensions/supervisor_shim.h @@ -16,7 +16,7 @@ #include inline char gSidecarPort[16] = "41841"; // node http port -> sidecar listen + unix-socket key -inline char gAntWalkerThreads[16] = "4"; // matches the node default; 0 keeps the walker unspawned +inline char gAntWalkerThreads[16] = "0"; // matches the node default; 0 keeps the walker unspawned // Forward a stop signal to the children so the container/service stops promptly. static void shimForwardSignal(int sig) diff --git a/src/qubic.cpp b/src/qubic.cpp index 56b7050d..ff42cd69 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -11801,7 +11801,7 @@ void processArgs(int argc, const char* argv[]) { ("fbas-warmup", "TEST: publish this many valid ant solutions before switching to the --fbas mode", cxxopts::value()->default_value("0")) ("fbas-gap", "TEST: minimum ticks between ant publishes; a gap wider than the fork window makes each window retire", cxxopts::value()->default_value("0")) ("ant-debug", "Trace ant-colony accepts, over-accepts and network rebuilds (budgeted per epoch)", cxxopts::value()) - ("ant-walker-threads", "Ant network walks handed to the walker sidecar (0=off)", cxxopts::value()->default_value("4")) + ("ant-walker-threads", "Ant network walks handed to the walker sidecar (0=off)", cxxopts::value()->default_value("0")) ("ant-walker-debug", "Trace every ant walker job and result", cxxopts::value()) #if defined(__linux__) && !defined(LITE_WASM_SC) ("verify-fork-rollback", "TEST: assert fork re-run reproduces quorum digest", cxxopts::value()) From 46e0ef531a8a7142f1777b8c38932f171daa83cf Mon Sep 17 00:00:00 2001 From: fnordspace Date: Wed, 2 Sep 2026 13:17:19 +0200 Subject: [PATCH 20/21] Revert "New version of Nostromo (#842)" This reverts commit 1d9fb4d6124ea3e1c21fe5eba7abf19dc3299bd3. --- src/contract_core/contract_def.h | 2 +- src/contracts/Nostromo.h | 7284 +++++------------------------- src/qpi/impl/qpi_system_impl.h | 33 +- src/qpi/qpi_context.h | 3 - test/contract_nostromo.cpp | 5647 +++++++---------------- 5 files changed, 2887 insertions(+), 10082 deletions(-) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 300adc1a..b44b4a9e 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -576,7 +576,7 @@ struct ContractStateChangeInfo // When enabling, replace both lines below, e.g.: //constexpr ContractStateChangeInfo contractStateChangeInfos[] = { { DUMMY_CONTRACT_INDEX, MIGRATE, 219 } }; //constexpr unsigned int contractStateChangeCount = sizeof(contractStateChangeInfos) / sizeof(contractStateChangeInfos[0]); -constexpr ContractStateChangeInfo contractStateChangeInfos[] = { { QIP_CONTRACT_INDEX, RESET, 224 }, { RANDOM_CONTRACT_INDEX, PADDING, 224 }, {NOST_CONTRACT_INDEX, MIGRATE, 229}}; +constexpr ContractStateChangeInfo contractStateChangeInfos[] = { { QIP_CONTRACT_INDEX, RESET, 224 }, { RANDOM_CONTRACT_INDEX, PADDING, 224 } }; constexpr unsigned int contractStateChangeCount = sizeof(contractStateChangeInfos) / sizeof(contractStateChangeInfos[0]); diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index e3e34c84..1e50441e 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -1,146 +1,29 @@ using namespace QPI; -namespace QPI -{ - inline bool operator==(const Asset& lhs, const Asset& rhs) - { - return lhs.assetName == rhs.assetName && lhs.issuer == rhs.issuer; - } -} // namespace QPI - -// Maximum number of active auction records stored by the contract, in auctions. -constexpr uint64 NOST_AUCTION_NUM = 2048; -// Number of full closed-auction snapshots retained in the history ring buffer, in entries. -constexpr uint64 NOST_AUCTION_HISTORY_NUM = 1024; -// Fixed length of an auction metadata IPFS CID, in bytes. -constexpr uint64 NOST_AUCTION_METADATA_CID_LENGTH = 64; -// Maximum number of active auction-participant bid records, in entries. -constexpr uint64 NOST_AUCTION_PARTICIPANT_NUM = 4096; -// Maximum number of wallets with unpaid QU obligations retained by the contract. -constexpr uint64 NOST_PENDING_PAYOUT_NUM = 8192; -// Maximum pending-payout slots one auction settlement may require before END_EPOCH fee distribution. -constexpr uint64 NOST_AUCTION_REVENUE_MAX_PAYOUT_RECIPIENTS = 1; -// Additional pending-payout slot reserved for the Batch bid caller's possible overpayment refund. -constexpr uint64 NOST_BATCH_BID_CALLER_PAYOUT_RECIPIENTS = 1; -// Maximum pending-payout slots reserved by a Standard bid for refunds and an immediate Buy Now settlement. -constexpr uint64 NOST_STANDARD_BID_MAX_PAYOUT_RECIPIENTS = 6; -// Maximum pending-payout slots reserved by Standard settlement for revenue distribution and bidder handling. -constexpr uint64 NOST_STANDARD_FINALIZATION_MAX_PAYOUT_RECIPIENTS = 5; -// Number of QPI-sized QU transfer chunks attempted for an immediate refund or settlement payout. -constexpr uint64 NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL = 1; -// Maximum number of pending-payout wallets retried automatically during one END_EPOCH call. -constexpr uint64 NOST_END_EPOCH_PAYOUT_RECIPIENT_NUM = 64; -// Number of QPI-sized QU transfer chunks retried per pending-payout wallet at END_EPOCH. -constexpr uint64 NOST_END_EPOCH_PAYOUT_CHUNKS_PER_RECIPIENT = 1; -// Maximum number of QPI-sized QU transfers attempted for one wallet in one procedure call. -constexpr uint64 NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL = 16; -// Sentinel for "no participant slot". -constexpr uint64 NOST_INVALID_PARTICIPANT_SLOT = NOST_AUCTION_PARTICIPANT_NUM; -// Maximum number of entries returned by one paginated auction getter call. -constexpr uint64 NOST_AUCTION_GETTER_PAGE_SIZE = 64; -// Maximum number of asset entries in a Batch Auction lot. -constexpr uint64 NOST_BATCH_AUCTION_LOT_ITEM_NUM = 1; -// Integer offset that makes the Batch coverage threshold include the first quantity below the minimum allocation. -constexpr uint64 NOST_BATCH_COVERAGE_THRESHOLD_OFFSET = 1; -// Maximum number of asset entries in a Standard Auction lot. -constexpr uint64 NOST_AUCTION_LOT_ITEM_NUM = 4; -// Maximum number of bidder wallets allowed by a private auction wallet gate. -constexpr uint64 NOST_AUCTION_ALLOWED_WALLET_NUM = 16; -// Maximum number of alternative assets accepted by a private auction asset gate. -constexpr uint64 NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM = 4; -// Maximum configured duration of any auction, in days. -constexpr uint32 NOST_AUCTION_MAX_DURATION_DAYS = 30; -// Default fee charged to create a private auction, in qu. -constexpr sint64 NOST_DEFAULT_PRIVATE_AUCTION_FEE = 50000000LL; -// Default fee accumulated after successfully creating a public auction and distributed at END_EPOCH, in qu. -constexpr sint64 NOST_PUBLIC_AUCTION_CREATION_FEE = 100LL; -// Minimum total payment target for small accepted Batch Auction bids, in qu. -constexpr uint64 NOST_BATCH_BID_FEE_CUTOFF = 100ULL; -// Default fee deducted when an auction is cancelled, in basis points. -constexpr uint64 NOST_DEFAULT_AUCTION_CANCELLATION_FEE_BP = 1000ULL; -// Default management fee applied to gross auction proceeds, in basis points. -constexpr uint64 NOST_DEFAULT_AUCTION_MANAGEMENT_FEE_BP = 50ULL; -// Default development fee applied to gross auction proceeds, in basis points. -constexpr uint64 NOST_DEFAULT_AUCTION_DEVELOPMENT_FEE_BP = 50ULL; -// Default takeover coordinator fee applied to gross auction proceeds, in basis points. -constexpr uint64 NOST_DEFAULT_AUCTION_TAKEOVER_COORDINATOR_FEE_BP = 50ULL; -// Shareholder allocation of auction creation, small-bid, and cancellation service fees, in basis points. -constexpr uint64 NOST_AUCTION_SERVICE_FEE_SHAREHOLDER_BP = 7270ULL; -// Management allocation of auction creation, small-bid, and cancellation service fees, in basis points. -constexpr uint64 NOST_AUCTION_SERVICE_FEE_MANAGEMENT_BP = 910ULL; -// Development allocation of auction creation, small-bid, and cancellation service fees, in basis points. -constexpr uint64 NOST_AUCTION_SERVICE_FEE_DEVELOPMENT_BP = 910ULL; -// Takeover coordinator allocation of auction creation, small-bid, and cancellation service fees, in basis points. -constexpr uint64 NOST_AUCTION_SERVICE_FEE_TAKEOVER_COORDINATOR_BP = 910ULL; -// Default portion of the shareholder fee distributed as dividends, in basis points. -constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_DIVIDEND_BP = 9000ULL; -// Default shareholder fee for gross proceeds in tier 1, in basis points. -constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_1 = 500ULL; -// Default shareholder fee for gross proceeds in tier 2, in basis points. -constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_2 = 450ULL; -// Default shareholder fee for gross proceeds in tier 3, in basis points. -constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_3 = 400ULL; -// Default shareholder fee for gross proceeds in tier 4, in basis points. -constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4 = 350ULL; -// Inclusive upper gross-proceeds threshold for shareholder fee tier 1, in qu. -constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1 = 5000000000ULL; -// Inclusive upper gross-proceeds threshold for shareholder fee tier 2, in qu. -constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2 = 50000000000ULL; -// Inclusive upper gross-proceeds threshold for shareholder fee tier 3, in qu. -constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3 = 200000000000ULL; -// Time added when an accepted bid arrives near an auction deadline, in seconds. -constexpr uint64 NOST_AUCTION_EXTENSION_SECONDS = 300ULL; -// Number of seconds used to convert one auction duration day. -constexpr uint64 NOST_SECONDS_PER_DAY = 86400ULL; -// Time allowed for a Standard Auction seller to resolve a pending sale, in seconds. -constexpr uint64 NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS = 604800ULL; -// Duration of the scheduled auction pause before an epoch transition, in seconds. -constexpr uint64 NOST_AUCTION_PRE_EPOCH_PAUSE_SECONDS = 1800ULL; -// Duration of the auction launch pause after `BEGIN_EPOCH`, in ticks. -constexpr uint32 NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS = 500U; -// Denominator representing 100 percent in basis-point calculations. -constexpr uint64 NOST_BASIS_POINTS_SCALE = 10000ULL; -// Number of microseconds used to convert a timestamp duration to seconds. -constexpr uint64 NOST_MICROSECONDS_PER_SECOND = 1000000ULL; -// Epoch at which the contract reapplies its default configuration, in epochs. -constexpr uint16 NOST_REINITIALIZATION_EPOCH = 220U; -// Quantity used to sell a Standard Auction lot as one indivisible unit, not an asset count. -constexpr uint64 NOST_STANDARD_AUCTION_LOT_COUNT = 1ULL; -// Minimum allowed Standard Auction starting and sale price, in qu. -constexpr uint64 NOST_STANDARD_MIN_PRICE = 1000000ULL; -// Minimum allowed Standard Auction bid increment, in qu. -constexpr uint64 NOST_STANDARD_MIN_BID_INCREMENT = 1000ULL; -// Year component of the packed initial date stamp. -constexpr uint8 NOST_DEFAULT_INIT_YEAR = 22U; -// Month component of the packed initial date stamp. -constexpr uint8 NOST_DEFAULT_INIT_MONTH = 4U; -// Day component of the packed initial date stamp. -constexpr uint8 NOST_DEFAULT_INIT_DAY = 13U; -// Bit offset of the year component in a packed date stamp, in bits. -constexpr uint8 NOST_DATE_STAMP_YEAR_SHIFT = 9U; -// Bit offset of the month component in a packed date stamp, in bits. -constexpr uint8 NOST_DATE_STAMP_MONTH_SHIFT = 5U; -// Runtime day-of-week index on which the scheduled pre-epoch pause begins. -constexpr uint8 NOST_PRE_EPOCH_PAUSE_DAY_OF_WEEK = 0U; -// UTC hour at which the scheduled pre-epoch pause begins. -constexpr uint8 NOST_PRE_EPOCH_PAUSE_HOUR = 11U; -// Minute within the configured hour at which the scheduled pre-epoch pause begins. -constexpr uint8 NOST_PRE_EPOCH_PAUSE_MINUTE = 30U; -// Packed date stamp used to recognize the contract's initial runtime date. -constexpr uint32 NOST_DEFAULT_INIT_TIME = - NOST_DEFAULT_INIT_YEAR << NOST_DATE_STAMP_YEAR_SHIFT | NOST_DEFAULT_INIT_MONTH << NOST_DATE_STAMP_MONTH_SHIFT | NOST_DEFAULT_INIT_DAY; -// Default enabled flag that routes all collected auction fees to development. -constexpr uint8 NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT = 1; -// Default drop in the execution fee reserve that triggers an emergency pause, in basis points. -constexpr uint64 NOST_DEFAULT_FEE_RESERVE_GUARD_DROP_BP = 1000ULL; -// Default rolling window used to evaluate the execution fee reserve drop, in seconds. -constexpr uint64 NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS = 600ULL; - -/** Old */ -constexpr uint32 NOSTROMO_MAX_USER_OLD = 262144; -constexpr uint32 NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST_OLD = 128; -constexpr uint32 NOSTROMO_MAX_NUMBER_TOKEN_OLD = 262144; -constexpr uint32 NOSTROMO_MAX_NUMBER_PROJECT_OLD = 262144; +constexpr uint64 NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT = 20000000ULL; +constexpr uint64 NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT = 100000000ULL; +constexpr uint64 NOSTROMO_TIER_DOG_STAKE_AMOUNT = 200000000ULL; +constexpr uint64 NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT = 800000000ULL; +constexpr uint64 NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT = 3200000000ULL; +constexpr uint64 NOSTROMO_QX_TOKEN_ISSUANCE_FEE = 1000000000ULL; + +constexpr uint32 NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT = 55; +constexpr uint32 NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT = 300; +constexpr uint32 NOSTROMO_TIER_DOG_POOL_WEIGHT = 750; +constexpr uint32 NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT = 3050; +constexpr uint32 NOSTROMO_TIER_WARRIOR_POOL_WEIGHT = 13750; + +constexpr uint32 NOSTROMO_TIER_FACEHUGGER_UNSTAKE_FEE = 5; +constexpr uint32 NOSTROMO_TIER_CHESTBURST_UNSTAKE_FEE = 4; +constexpr uint32 NOSTROMO_TIER_DOG_UNSTAKE_FEE = 3; +constexpr uint32 NOSTROMO_TIER_XENOMORPH_UNSTAKE_FEE = 2; +constexpr uint32 NOSTROMO_TIER_WARRIOR_UNSTAKE_FEE = 1; +constexpr uint32 NOSTROMO_CREATE_PROJECT_FEE = 100000000; + +constexpr uint32 NOSTROMO_MAX_USER = 262144; +constexpr uint32 NOSTROMO_MAX_NUMBER_PROJECT = 262144; +constexpr uint32 NOSTROMO_MAX_NUMBER_TOKEN = 262144; +constexpr uint32 NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST = 128; struct NOST2 { @@ -148,6369 +31,1618 @@ struct NOST2 struct NOST : public ContractBase { - enum class EProcedureId : uint8 +public: + /****** PORTED TIMEUTILS FROM OLD Nostromo *****/ + /** + * Compare 2 date in uint32 format + * @return -1 lesser(ealier) AB + */ + inline static sint32 dateCompare(uint32& A, uint32& B, sint32& i) { - CreateAuction = 1, - PlaceBid = 2, - CancelAuction = 3, - TransferShareManagementRights = 4, - ResolvePendingStandardAuction = 5, - SetAuctionFees = 6, - SetAuctionFeesByManagement = 7, - SetManagement = 8, - SetFeeReserveGuardConfig = 9, - SetEmergencyPause = 10 - }; + if (A == B) return 0; + if (A < B) return -1; + return 1; + } - /** @brief Stable public-function identifiers used by the contract ABI. */ - enum class EFunctionId : uint16 + /** + * @return pack Nost datetime data from year, month, day, hour, minute, second to a uint32 + * year is counted from 24 (2024) + */ + inline static void packNostromoDate(uint32 _year, uint32 _month, uint32 _day, uint32 _hour, uint32 _minute, uint32 _second, uint32& res) { - GetAuctionByIndex = 1, - GetAuctionParticipant = 2, - GetTicksBeforeAuctionLaunch = 3, - GetAuctionFees = 4, - GetFeeRecipients = 5, - GetClosedAuctionHistory = 6, - GetRouteAllFeesToDevelopment = 7, - GetContractStats = 8, - GetAuctionSummaries = 9, - GetActiveAuctionIndices = 10, - GetAuctionsBySeller = 11, - GetAuctionByMetadataCid = 12, - GetAuctionSummariesByIndexBatch = 13, - GetAuctionParticipants = 14, - GetUserParticipations = 15, - GetLatestAuctionIndex = 16, - GetAuctionCountBySeller = 17, - GetAuctionAtCreationSnapshot = 18, - GetBatchAuctionBidAvailability = 19, - CalculateBatchAuctionBidFee = 20, - GetPendingServiceFeePool = 21, - GetFeeReserveGuardState = 22, - GetPendingPayout = 23, - GetNostromoFeePool = 24 - }; + res = ((_year - 24) << 26) | (_month << 22) | (_day << 17) | (_hour << 12) | (_minute << 6) | (_second); + } - enum class EAuctionType : uint8 + inline static uint32 NostGetYear(uint32 data) { - None, - Batch, - Standard - }; - - enum class EAuctionVisibility : uint8 + return ((data >> 26) + 24); + } + inline static uint32 NostGetMonth(uint32 data) { - None, - Public, - Private - }; - - enum class EAuctionStatus : uint8 + return ((data >> 22) & 0b1111); + } + inline static uint32 NostGetDay(uint32 data) { - None, - Active, - Finalized, - Cancelled, - PendingSellerDecision - }; - - enum class EAuctionError : uint8 + return ((data >> 17) & 0b11111); + } + inline static uint32 NostGetHour(uint32 data) { - Success, - InvalidInput, - AuctionNotFound, - AuctionClosed, - Forbidden, - InsufficientFunds, - InsufficientAssetBalance, - StorageFull, - InvalidAuctionType, - InvalidVisibility, - BidTooLow, - PrivateAuctionAccessDenied, - AuctionPaused, - AuctionIndexExhausted, - QuantityUnavailable, - AuctionHasAcceptedBid, - PayoutQueueFull - }; - - /** - * @brief Stores one bid slot in one auction. - * @note The same struct is shared by batch and standard auctions. - */ - struct AuctionParticipantData + return ((data >> 12) & 0b11111); + } + inline static uint32 NostGetMinute(uint32 data) { - /** @brief Auction that owns this bid slot. */ - uint64 auctionIndex; - - /** @brief Monotonic bid sequence inside the auction, used for FIFO tie-breaks. */ - uint64 bidIndex; - - /** @brief Amount currently locked in escrow for the participant bid. */ - uint64 escrowedAmount; - - /** @brief Quantity requested by the participant; standard auctions always use the whole lot quantity. */ - uint64 requestedQuantity; - - /** @brief Quantity finally allocated to the participant after batch auction settlement. */ - uint64 allocatedQuantity; - - /** @brief Offered price per asset in a batch auction, or total offered price for the whole lot in a standard auction. */ - uint64 bidAmount; - - /** @brief Wallet that owns this participant record. */ - id participant; - - /** @brief Timestamp of the participant's latest accepted bid. */ - DateAndTime lastBidTime; - - /** @brief Marks whether this fixed array slot contains a reusable historical or active record. */ - uint8 isUsed; - - /** @brief Marks bids that are still eligible for allocation or standard highest-bid settlement. */ - uint8 isActive; - - /** @brief Marks bids that remain inside the winning allocation after settlement. */ - uint8 isWinningBid; - }; - - /** - * @brief Describes an asset and quantity used by an auction lot or private access rule. - */ - struct AuctionAssetEntry + return ((data >> 6) & 0b111111); + } + inline static uint32 NostGetSecond(uint32 data) { - /** @brief Asset included in a lot or used as an access requirement. */ - Asset asset; - - /** @brief Lot quantity or minimum ownership quantity required for access. */ - sint64 quantity; - }; - - /** - * @brief Shared auction fields used by persistent state and public getter views. - * @note Container fields differ between persistent state and ABI views, so access-control collections stay outside this struct. - * @note `metadataIpfsCid` points to off-chain auction metadata stored in IPFS. - * @note `sellerDecisionDeadline` stays zero until a standard auction enters the manual decision window. - */ - struct AuctionCore + return (data & 0b111111); + } + /* + * @return unpack Nost datetime from uin32 to year, month, day, hour, minute, secon + */ + inline static void unpackNostromoDate(uint8& _year, uint8& _month, uint8& _day, uint8& _hour, uint8& _minute, uint8& _second, uint32 data) { - /** @brief Assets and quantities offered by the auction. */ - Array auctionLotItems; - - /** @brief Lowercase base32 CIDv1 stored in Pinata for the auction name and description metadata. */ - Array metadataIpfsCid; - - /** @brief Wallet that created the auction and offers the lot for sale. */ - id seller; - - /** @brief Wallet that currently holds the highest bid. */ - id highestBidder; - - /** @brief Timestamp when the seller created the auction. */ - DateAndTime createdAt; - - /** @brief Timestamp of the most recent accepted bid. */ - DateAndTime lastBidAt; - - /** @brief Deadline for the seller to accept or reject a standard auction bid that ended between Initial Price and Sale Price. */ - DateAndTime sellerDecisionDeadline; - - /** @brief Timestamp when the auction was finalized, cancelled, or otherwise settled. */ - DateAndTime settledAt; - - /** @brief Total sale units offered; batch auctions use asset quantity, standard auctions use one unit for the whole lot. */ - uint64 quantityForSale; - - /** @brief Quantity already assigned to winning bids after settlement. */ - uint64 allocatedQuantity; - - /** @brief Minimum quantity requested by each batch bid; always zero for standard auctions. */ - uint64 minimumPurchaseQuantity; - - /** @brief Initial price for a standard auction; bids cannot start below this total price for the whole lot. */ - uint64 initialPrice; - - /** @brief Minimum selling price per asset in a batch auction, or desired minimum total selling price for the whole lot in a standard - * auction. */ - uint64 salePrice; - - /** @brief Minimum increment by which a new standard auction bid must exceed the current highest bid. */ - uint64 minimumBidIncrement; - - /** @brief Buy Now price that closes a standard auction immediately when matched or exceeded. */ - uint64 buyNowPrice; - - /** @brief Highest offered price per asset in a batch auction, or highest total offered price in a standard auction. */ - uint64 highestBidPrice; - - /** @brief Quantity requested by the current highest bid. */ - uint64 highestBidQuantity; - - /** @brief Total amount escrowed by the current highest bid; equal to the committed highest bid amount. */ - uint64 highestBidAmount; - - /** @brief Auction duration in seconds, derived from the duration configured in days. */ - uint64 auctionDurationSeconds; - - /** @brief Monotonic identifier assigned when the auction is created. */ - uint64 auctionIndex; - - /** @brief Monotonic per-auction bid index used to store every batch bid as a separate position. */ - uint64 nextBidIndex; - - /** @brief Fixed-array slot of the current standard-auction highest bid, or `NOST_INVALID_PARTICIPANT_SLOT`. */ - uint64 highestBidSlotIndex; - - /** @brief Auction House mode: Batch Auction or Standard Auction. */ - EAuctionType type; - - /** @brief Auction visibility: public or restricted private access. */ - EAuctionVisibility visibility; - - /** @brief Current lifecycle status of the auction, including the seller decision phase for standard auctions. */ - EAuctionStatus status; - }; + _year = NostGetYear(data); // 6 bits + _month = NostGetMonth(data); //4bits + _day = NostGetDay(data); //5bits + _hour = NostGetHour(data); //5bits + _minute = NostGetMinute(data); //6bits + _second = NostGetSecond(data); //6bits + } - /** - * @brief Stores all persistent data for one auction. - * @note The same struct is shared by batch and standard auctions. - */ - struct AuctionData + inline static void accumulatedDay(sint32 month, uint64& res) { - /** @brief Fields shared with the public auction view. */ - AuctionCore core; - - /** @brief Wallet whitelist used when the private auction uses wallet-based access. */ - HashSet allowedBidderWallets; - - /** @brief Minimum quantity by asset required for participation; owning any one entry grants access. */ - HashMap requiredAccessAssets; - }; - + switch (month) + { + case 1: res = 0; break; + case 2: res = 31; break; + case 3: res = 59; break; + case 4: res = 90; break; + case 5: res = 120; break; + case 6: res = 151; break; + case 7: res = 181; break; + case 8: res = 212; break; + case 9: res = 243; break; + case 10:res = 273; break; + case 11:res = 304; break; + case 12:res = 334; break; + } + } /** - * @brief Serializable view of one auction for public getter outputs. - * @note Persistent state uses `HashSet` for access checks, but ABI payloads expose fixed arrays because `HashSet` is not valid in - * input/output structs. + * @return difference in number of second, A must be smaller than or equal B to have valid value */ - struct AuctionView + inline static void diffDateInSecond(uint32& A, uint32& B, sint32& i, uint64& dayA, uint64& dayB, uint64& res) { - /** @brief Fields shared with the persistent auction record. */ - AuctionCore core; + if (dateCompare(A, B, i) >= 0) + { + res = 0; + return; + } + accumulatedDay(NostGetMonth(A), dayA); + dayA += NostGetDay(A); + accumulatedDay(NostGetMonth(B), dayB); + dayB += (NostGetYear(B) - NostGetYear(A)) * 365ULL + NostGetDay(B); - /** @brief Wallet list used when the private auction restricts participation to predefined wallets. */ - Array allowedBidderWallets; + // handling leap-year: only store last 2 digits of year here, don't care about mod 100 & mod 400 case + for (i = NostGetYear(A); (uint32)(i) < NostGetYear(B); i++) + { + if (mod(i, 4) == 0) + { + dayB++; + } + } + if (mod(sint32(NostGetYear(A)), 4) == 0 && (NostGetMonth(A) > 2)) dayA++; + if (mod(sint32(NostGetYear(B)), 4) == 0 && (NostGetMonth(B) > 2)) dayB++; + res = (dayB - dayA) * 3600ULL * 24; + res += (NostGetHour(B) * 3600 + NostGetMinute(B) * 60 + NostGetSecond(B)); + res -= (NostGetHour(A) * 3600 + NostGetMinute(A) * 60 + NostGetSecond(A)); + } - /** @brief Asset and minimum-quantity alternatives used by private asset-based access. */ - Array requiredAccessAssets; + inline static bool checkValidNostDateTime(uint32& A) + { + if (NostGetMonth(A) > 12) return false; + if (NostGetDay(A) > 31) return false; + if ((NostGetDay(A) == 31) && + (NostGetMonth(A) != 1) && (NostGetMonth(A) != 3) && (NostGetMonth(A) != 5) && + (NostGetMonth(A) != 7) && (NostGetMonth(A) != 8) && (NostGetMonth(A) != 10) && (NostGetMonth(A) != 12)) return false; + if ((NostGetDay(A) == 30) && (NostGetMonth(A) == 2)) return false; + if ((NostGetDay(A) == 29) && (NostGetMonth(A) == 2) && (mod(NostGetYear(A), 4u) != 0)) return false; + if (NostGetHour(A) >= 24) return false; + if (NostGetMinute(A) >= 60) return false; + if (NostGetSecond(A) >= 60) return false; + return true; + } - /** @brief Number of populated entries in `requiredAccessAssets`. */ - uint64 requiredAccessAssetCount; + /****** END PORTED TIMEUTILS FROM OLD Nostromo *****/ - /** @brief Number of populated entries in `allowedBidderWallets`. */ - uint64 allowedBidderWalletCount; + struct investInfo + { + uint64 investedAmount; + uint64 claimedAmount; + uint32 indexOfFundraising; }; - struct OldStateData + struct projectInfo { - struct investInfo - { - uint64 investedAmount; - uint64 claimedAmount; - uint32 indexOfFundraising; - }; - - struct projectInfo - { - id creator; - uint64 tokenName; - uint64 supplyOfToken; - uint32 startDate; - uint32 endDate; - uint32 numberOfYes; - uint32 numberOfNo; - bit isCreatedFundarasing; - }; - - struct fundaraisingInfo - { - uint64 tokenPrice; - uint64 soldAmount; - uint64 requiredFunds; - uint64 raisedFunds; - uint32 indexOfProject; - uint32 firstPhaseStartDate; - uint32 firstPhaseEndDate; - uint32 secondPhaseStartDate; - uint32 secondPhaseEndDate; - uint32 thirdPhaseStartDate; - uint32 thirdPhaseEndDate; - uint32 listingStartDate; - uint32 cliffEndDate; - uint32 vestingEndDate; - uint8 threshold; - uint8 TGE; - uint8 stepOfVesting; - bit isCreatedToken; - }; - - HashMap users; - HashMap, NOSTROMO_MAX_USER_OLD> voteStatus; - HashMap numberOfVotedProject; - HashSet tokens; - - HashMap, NOSTROMO_MAX_USER_OLD> investors; - HashMap numberOfInvestedProjects; - Array tmpInvestedList; - - Array projects; - - Array fundaraisings; - - id teamAddress; - sint64 transferRightsFee; - uint64 epochRevenue, totalPoolWeight; - uint32 numberOfRegister, numberOfCreatedProject, numberOfFundraising; + id creator; + uint64 tokenName; + uint64 supplyOfToken; + uint32 startDate; + uint32 endDate; + uint32 numberOfYes; + uint32 numberOfNo; + bit isCreatedFundarasing; }; - /** - * @brief Epoch fee accrual shared by Nostromo modules. - * @note Auction shareholder amounts are separated by sale tier because their fee formulas differ. Other recipient amounts are compatible sums. - */ - struct NostromoFeePool + struct fundaraisingInfo { - uint64 shareholderDividendTier1Amount; - uint64 shareholderDividendTier2Amount; - uint64 shareholderDividendTier3Amount; - uint64 shareholderDividendTier4Amount; - uint64 commonServiceFeeAmount; - uint64 shareholderDividendAmount; - uint64 managementAmount; - uint64 developmentAmount; - uint64 takeoverCoordinatorAmount; + uint64 tokenPrice; + uint64 soldAmount; + uint64 requiredFunds; + uint64 raisedFunds; + uint32 indexOfProject; + uint32 firstPhaseStartDate; + uint32 firstPhaseEndDate; + uint32 secondPhaseStartDate; + uint32 secondPhaseEndDate; + uint32 thirdPhaseStartDate; + uint32 thirdPhaseEndDate; + uint32 listingStartDate; + uint32 cliffEndDate; + uint32 vestingEndDate; + uint8 threshold; + uint8 TGE; + uint8 stepOfVesting; + bit isCreatedToken; }; struct StateData { - /** @brief Configured fee charged when creating a private auction. */ - sint64 privateAuctionFee; - - /** @brief Configured non-negative fee accumulated when creating a public auction and distributed at `END_EPOCH`. */ - sint64 publicAuctionCreationFee; - - /** @brief Configured cancellation fee rate in basis points. */ - uint64 auctionCancellationFeeBasisPoints; - - /** @brief Undistributed shareholder revenue from the shared fee pool reserved for contract dividends. */ - uint64 auctionShareholderDividendPool; - - /** @brief Configured management fee rate in basis points, charged from auction proceeds. */ - uint64 managementFeeBasisPoints; - - /** @brief Configured development fee rate in basis points, charged from auction proceeds. */ - uint64 developmentFeeBasisPoints; - - /** @brief Configured takeover coordinator fee rate in basis points, charged from auction proceeds. */ - uint64 takeoverCoordinatorFeeBasisPoints; - - /** @brief Share of the shareholder fee redirected to dividends, expressed in basis points. */ - uint64 shareholderDividendBasisPoints; - - /** @brief Shareholder fee tier applied to auctions up to the first threshold. */ - uint64 shareholderFeeBasisPointsTier1; - - /** @brief Shareholder fee tier applied to auctions above the first threshold and up to the second threshold. */ - uint64 shareholderFeeBasisPointsTier2; - - /** @brief Shareholder fee tier applied to auctions above the second threshold and up to the third threshold. */ - uint64 shareholderFeeBasisPointsTier3; - - /** @brief Shareholder fee tier applied to auctions above the third threshold. */ - uint64 shareholderFeeBasisPointsTier4; - - /** @brief Start of the currently active global auction timer pause interval. */ - DateAndTime auctionTimerPauseStartedAt; - - /** @brief End of the currently active global auction timer pause interval. */ - DateAndTime auctionTimerPauseEndsAt; - - /** @brief Configured maximum auction duration in days. */ - uint32 maxAuctionDurationDays; - - /** @brief Cached QX transfer fee refreshed at the beginning of each epoch. */ - uint32 qxTransferFee; - - /** @brief Flag indicating whether the post-`BEGIN_EPOCH()` auction pause is active for the current epoch. */ - uint8 isPostBeginEpochPauseArmed; - - /** @brief Flag indicating whether auction deadlines are currently frozen by a global pause interval. */ - uint8 isAuctionTimerPaused; - - /** @brief Flag indicating whether every auction fee is routed to the development wallet. */ - uint8 routeAllFeesToDevelopment; - - id management; - - id development; - - id takeoverCoordinator; - - /** @brief Total number of auctions ever created; also the next auction index. */ - uint64 totalAuctionsCreated; + HashMap users; + HashMap, NOSTROMO_MAX_USER> voteStatus; + HashMap numberOfVotedProject; + HashSet tokens; - /** @brief Circular buffer with full snapshots of finalized and cancelled auctions. */ - Array closedAuctionHistory; + HashMap, NOSTROMO_MAX_USER> investors; + HashMap numberOfInvestedProjects; + Array tmpInvestedList; - /** @brief Monotonic insertion counter for `closedAuctionHistory`. */ - uint64 closedAuctionHistoryCounter; + Array projects; - HashMap auctionList; - /** @brief Active bid records; slots are cleared as soon as the bid leaves the live order book. */ - Array participants; - /** @brief Bounded history of completed, refunded, and displaced bid records. */ - Array participantHistory; - /** @brief Monotonic insertion counter for `participantHistory`. */ - uint64 participantHistoryCounter; + Array fundaraisings; - /** @brief Wallet-indexed QU liabilities registered before an auction is finalized. */ - HashMap pendingQuPayouts; - /** @brief Sum of all values in `pendingQuPayouts`, in qu. */ - uint64 totalPendingQuPayouts; - /** @brief Physical hash-map slot from which the next bounded automatic payout scan starts. */ - uint64 pendingPayoutScanCursor; - /** @brief Lifetime number of finalized auctions. */ - uint64 totalFinalizedAuctions; - /** @brief Lifetime number of cancelled auctions. */ - uint64 totalCancelledAuctions; - - /** @brief Shared fee accrual for Auction House and future Nostromo modules, settled at `END_EPOCH`. */ - NostromoFeePool feePool; - - /** @brief Configured drop in the execution fee reserve that triggers an emergency pause, in basis points. */ - uint64 feeReserveGuardDropBasisPoints; - - /** @brief Configured rolling window used to evaluate the execution fee reserve drop, in seconds. */ - uint64 feeReserveGuardWindowSeconds; - - /** @brief Execution fee reserve value recorded at the start of the current guard window. */ - sint64 feeReserveBaseline; - - /** @brief Start of the current guard window; invalid when the window has not been initialized. */ - DateAndTime feeReserveBaselineAt; - - /** @brief Timestamp at which the emergency pause was triggered; invalid when not paused. */ - DateAndTime emergencyPausedAt; - - /** @brief Flag indicating whether an emergency pause is currently blocking auction interactions. */ - uint8 isEmergencyPaused; + id teamAddress; + sint64 transferRightsFee; + uint64 epochRevenue, totalPoolWeight; + uint32 numberOfRegister, numberOfCreatedProject, numberOfFundraising; }; - /** @brief Input payload used to create a Batch Auction or Standard Auction in the Auction House. */ - struct CreateAuction_input + struct registerInTier_input { - /** @brief Lowercase base32 CIDv1 stored in Pinata for the auction name and description metadata. */ - Array metadataIpfsCid; - - /** @brief Assets and quantities offered by the auction. */ - Array auctionLotItems; - - /** @brief Asset and minimum-quantity alternatives used by private asset-based access. */ - Array requiredAccessAssets; - - /** @brief Wallet list used when the private auction restricts participation to predefined wallets. */ - Array allowedBidderWallets; - - /** @brief Required minimum requested quantity for batch bids; ignored for standard auctions. */ - uint64 minimumPurchaseQuantity; - - /** @brief Initial price for a standard auction; bids cannot be placed below this total price for the whole lot. */ - uint64 initialPrice; - - /** @brief Minimum selling price per asset in a batch auction, or desired minimum total selling price for the whole lot in a standard - * auction. */ - uint64 salePrice; - - /** @brief Minimum increment by which each new standard auction bid must exceed the current highest bid. */ - uint64 minimumBidIncrement; - - /** @brief Buy Now price that immediately closes a standard auction once matched or exceeded. */ - uint64 buyNowPrice; - - /** @brief Auction duration in days, capped by the contract configuration. */ - uint32 durationDays; - - /** @brief Auction House mode selected by the seller: Batch Auction or Standard Auction. */ - uint8 auctionType; - - /** @brief Visibility selected by the seller: public or private. */ - uint8 auctionVisibility; + uint32 tierLevel; }; - /** @brief Result of auction creation. */ - struct CreateAuction_output + struct registerInTier_output { - /** @brief Monotonic index assigned to the new auction when creation succeeds. */ - uint64 auctionIndex; - - /** @brief Result code describing whether the auction creation succeeded. */ - EAuctionError errorCode; + uint32 tierLevel; }; - /** @brief Input payload used to place a bid in a Batch Auction or Standard Auction. */ - struct PlaceBid_input + struct logoutFromTier_input { - /** @brief Monotonic index of the target auction. */ - uint64 auctionIndex; - - /** @brief Requested quantity for a batch auction, which must meet its configured minimum; ignored for a standard auction. */ - uint64 quantity; - /** @brief Offered price per asset in a batch auction, or total offered price for the whole lot in a standard auction. */ - uint64 bidAmount; }; - /** @brief Result of a bid placement request. */ - struct PlaceBid_output + struct logoutFromTier_output { - /** @brief Amount that remains escrowed for the accepted bid. */ - uint64 escrowedAmount; - - /** @brief Amount refunded to the bidder, including replaced escrow or invocation change. */ - uint64 refundedAmount; - - /** @brief Result code describing whether the bid placement succeeded. */ - EAuctionError errorCode; + bit result; }; - /** @brief Input payload used to cancel an active auction. */ - struct CancelAuction_input + struct createProject_input { - /** @brief Monotonic index of the auction that the seller wants to cancel. */ - uint64 auctionIndex; + uint64 tokenName; + uint64 supply; + uint32 startYear; + uint32 startMonth; + uint32 startDay; + uint32 startHour; + uint32 endYear; + uint32 endMonth; + uint32 endDay; + uint32 endHour; }; - /** @brief Result of an auction cancellation request. */ - struct CancelAuction_output + struct createProject_output { - /** @brief Total amount refunded to bidders because of the cancellation. */ - uint64 refundedAmount; - - /** @brief Cancellation fee charged to the seller according to the auction rules. */ - uint64 cancellationFee; - - /** @brief Result code describing whether the cancellation succeeded. */ - EAuctionError errorCode; + uint32 indexOfProject; }; - /** @brief Input payload used by the seller to accept or reject a pending standard auction result. */ - struct ResolvePendingStandardAuction_input + struct voteInProject_input { - /** @brief Monotonic index of the standard auction awaiting the seller decision. */ - uint64 auctionIndex; - - /** @brief Set to `1` to accept the sale or `0` to reject it. */ - uint8 acceptSale; + uint32 indexOfProject; + bit decision; }; - /** @brief Result of a seller decision on a pending standard auction. */ - struct ResolvePendingStandardAuction_output + struct voteInProject_output { - /** @brief Amount refunded to the bidder when the seller rejects the sale. */ - uint64 refundedAmount; - /** @brief Result code describing whether the seller decision was applied. */ - EAuctionError errorCode; }; - /** @brief Input payload used by the takeover coordinator to overwrite the full auction fee configuration. */ - struct SetAuctionFees_input + struct createFundraising_input { - /** @brief Fee charged when a private auction is created. */ - sint64 privateAuctionFee; - - /** @brief Non-negative fee accumulated when a public auction is created and distributed at `END_EPOCH`. */ - sint64 publicAuctionCreationFee; + uint64 tokenPrice; + uint64 soldAmount; + uint64 requiredFunds; - /** @brief Cancellation fee rate in basis points. */ - uint64 auctionCancellationFeeBasisPoints; + uint32 indexOfProject; + uint32 firstPhaseStartYear; + uint32 firstPhaseStartMonth; + uint32 firstPhaseStartDay; + uint32 firstPhaseStartHour; + uint32 firstPhaseEndYear; + uint32 firstPhaseEndMonth; + uint32 firstPhaseEndDay; + uint32 firstPhaseEndHour; - /** @brief Management fee rate in basis points. */ - uint64 managementFeeBasisPoints; + uint32 secondPhaseStartYear; + uint32 secondPhaseStartMonth; + uint32 secondPhaseStartDay; + uint32 secondPhaseStartHour; + uint32 secondPhaseEndYear; + uint32 secondPhaseEndMonth; + uint32 secondPhaseEndDay; + uint32 secondPhaseEndHour; - /** @brief Development fee rate in basis points. */ - uint64 developmentFeeBasisPoints; + uint32 thirdPhaseStartYear; + uint32 thirdPhaseStartMonth; + uint32 thirdPhaseStartDay; + uint32 thirdPhaseStartHour; + uint32 thirdPhaseEndYear; + uint32 thirdPhaseEndMonth; + uint32 thirdPhaseEndDay; + uint32 thirdPhaseEndHour; - /** @brief Takeover coordinator fee rate in basis points. */ - uint64 takeoverCoordinatorFeeBasisPoints; + uint32 listingStartYear; + uint32 listingStartMonth; + uint32 listingStartDay; + uint32 listingStartHour; - /** @brief Percentage of the shareholder fee distributed as dividends, in basis points. */ - uint64 shareholderDividendBasisPoints; + uint32 cliffEndYear; + uint32 cliffEndMonth; + uint32 cliffEndDay; + uint32 cliffEndHour; - /** @brief Shareholder fee tier for auctions up to the first threshold. */ - uint64 shareholderFeeBasisPointsTier1; + uint32 vestingEndYear; + uint32 vestingEndMonth; + uint32 vestingEndDay; + uint32 vestingEndHour; - /** @brief Shareholder fee tier for auctions above the first threshold and up to the second threshold. */ - uint64 shareholderFeeBasisPointsTier2; + uint8 threshold; + uint8 TGE; + uint8 stepOfVesting; + }; - /** @brief Shareholder fee tier for auctions above the second threshold and up to the third threshold. */ - uint64 shareholderFeeBasisPointsTier3; + struct createFundraising_output + { - /** @brief Shareholder fee tier for auctions above the third threshold. */ - uint64 shareholderFeeBasisPointsTier4; }; - struct SetAuctionFees_output + struct investInProject_input { - /** @brief Result code describing whether the fee update succeeded. */ - EAuctionError errorCode; + uint32 indexOfFundraising; }; - /** @brief Input payload used by management to update every fee except takeover coordinator-specific splits. */ - struct SetAuctionFeesByManagement_input + struct investInProject_output { - /** @brief Fee charged when a private auction is created. */ - sint64 privateAuctionFee; - /** @brief Non-negative fee accumulated when a public auction is created and distributed at `END_EPOCH`. */ - sint64 publicAuctionCreationFee; - - /** @brief Cancellation fee rate in basis points. */ - uint64 auctionCancellationFeeBasisPoints; - - /** @brief Management fee rate in basis points. */ - uint64 managementFeeBasisPoints; - - /** @brief Development fee rate in basis points. */ - uint64 developmentFeeBasisPoints; - - /** @brief Shareholder fee tier for auctions up to the first threshold. */ - uint64 shareholderFeeBasisPointsTier1; - - /** @brief Shareholder fee tier for auctions above the first threshold and up to the second threshold. */ - uint64 shareholderFeeBasisPointsTier2; - - /** @brief Shareholder fee tier for auctions above the second threshold and up to the third threshold. */ - uint64 shareholderFeeBasisPointsTier3; - - /** @brief Shareholder fee tier for auctions above the third threshold. */ - uint64 shareholderFeeBasisPointsTier4; }; - struct SetAuctionFeesByManagement_output + struct claimToken_input { - /** @brief Result code describing whether the fee update succeeded. */ - EAuctionError errorCode; + uint64 amount; + uint32 indexOfFundraising; }; - /** @brief Input payload used by the takeover coordinator to appoint a new management wallet. */ - struct SetManagement_input + struct claimToken_output { - /** @brief New wallet that will receive management privileges. */ - id management; + uint64 claimedAmount; }; - struct SetManagement_output + struct upgradeTier_input { - /** @brief Result code describing whether the management update succeeded. */ - EAuctionError errorCode; + uint32 newTierLevel; }; - /** @brief Input payload used by the takeover coordinator or management to configure the execution fee reserve guard. */ - struct SetFeeReserveGuardConfig_input + struct upgradeTier_output { - /** @brief Drop in the execution fee reserve, relative to the window baseline, that triggers an emergency pause, in basis points. */ - uint64 dropBasisPoints; - /** @brief Rolling window used to evaluate the execution fee reserve drop, in seconds. */ - uint64 windowSeconds; }; - struct SetFeeReserveGuardConfig_output + struct TransferShareManagementRights_input { - /** @brief Result code describing whether the guard configuration update succeeded. */ - EAuctionError errorCode; + Asset asset; + sint64 numberOfShares; + uint32 newManagingContractIndex; }; - - /** @brief Input payload used by the takeover coordinator or management to manually pause or resume auction interactions. */ - struct SetEmergencyPause_input + struct TransferShareManagementRights_output { - /** @brief Set to `1` to activate the emergency pause or `0` to resume normal operation. */ - uint8 paused; + sint64 transferredNumberOfShares; }; - struct SetEmergencyPause_output + struct getStats_input { - /** @brief Result code describing whether the emergency pause update succeeded. */ - EAuctionError errorCode; - }; - /** @brief Input payload used to fetch one auction from storage. */ - struct GetAuctionByIndex_input - { - /** @brief Monotonic index of the auction to read. */ - uint64 auctionIndex; }; - /** @brief Auction data returned by the read-only auction getter. */ - struct GetAuctionByIndex_output + struct getStats_output { - /** @brief Serializable auction data stored for the requested auction. */ - AuctionView auction; - - /** @brief Flag indicating whether the auction record exists. */ - uint8 found; + uint64 epochRevenue, totalPoolWeight; + uint32 numberOfRegister, numberOfCreatedProject, numberOfFundraising; }; - /** @brief Input payload used to fetch one participant record from an auction. */ - struct GetAuctionParticipant_input + struct getTierLevelByUser_input { - /** @brief Monotonic index of the auction that owns the participant record. */ - uint64 auctionIndex; - - /** @brief Wallet whose participant record should be returned. */ - id participant; + id userId; }; - /** @brief Participant data returned by the read-only participant getter. */ - struct GetAuctionParticipant_output + struct getTierLevelByUser_output { - /** @brief Participant record for the requested wallet in the requested auction. */ - AuctionParticipantData participantData; - - /** @brief Flag indicating whether the participant record exists. */ - uint8 found; + uint8 tierLevel; }; - /** @brief Input payload used to query the remaining post-BEGIN_EPOCH auction launch pause. */ - using GetTicksBeforeAuctionLaunch_input = NoData; - - /** @brief Result returned by the auction launch pause getter. */ - struct GetTicksBeforeAuctionLaunch_output + struct getUserVoteStatus_input { - /** @brief Number of ticks remaining before auction interactions resume after `BEGIN_EPOCH`. */ - uint32 ticks; + id userId; }; - /** @brief Input payload used to read the current auction fee configuration. */ - /** @brief Input payload used to read the amount of accumulated service fees awaiting distribution at `END_EPOCH`. */ - using GetPendingServiceFeePool_input = NoData; - - struct GetPendingServiceFeePool_output + struct getUserVoteStatus_output { - /** @brief Aggregate fee amount still awaiting `END_EPOCH` settlement. */ - uint64 pendingServiceFeePool; + uint32 numberOfVotedProjects; + Array projectIndexList; }; - /** @brief Input payload used to inspect the detailed shared Nostromo fee pool. */ - using GetNostromoFeePool_input = NoData; - - struct GetNostromoFeePool_output + struct checkTokenCreatability_input { - /** @brief Detailed fee accumulators that have not yet been moved to dividends or recipient payout liabilities. */ - NostromoFeePool feePool; - - /** @brief Aggregate of every amount in `feePool`. */ - uint64 totalAmount; + uint64 tokenName; }; - /** @brief Input used to inspect a wallet's registered QU payout. */ - struct GetPendingPayout_input + struct checkTokenCreatability_output { - /** @brief Wallet whose unpaid QU amount should be returned. */ - id account; + bit result; // result = 1 is the token already issued by SC }; - struct GetPendingPayout_output + struct getNumberOfInvestedProjects_input { - /** @brief QU currently owed to the requested wallet. */ - uint64 amount; + id userId; }; - /** @brief Input payload used to read the current state of the execution fee reserve guard. */ - using GetFeeReserveGuardState_input = NoData; - - struct GetFeeReserveGuardState_output + struct getNumberOfInvestedProjects_output { - /** @brief Live execution fee reserve value read from the system contract. */ - sint64 currentFeeReserve; - - /** @brief Execution fee reserve value recorded at the start of the current guard window. */ - sint64 feeReserveBaseline; - - /** @brief Start of the current guard window; invalid when the window has not been initialized. */ - DateAndTime feeReserveBaselineAt; - - /** @brief Timestamp at which the emergency pause was triggered; invalid when not paused. */ - DateAndTime emergencyPausedAt; - - /** @brief Configured drop in the execution fee reserve that triggers an emergency pause, in basis points. */ - uint64 dropBasisPoints; - - /** @brief Configured rolling window used to evaluate the execution fee reserve drop, in seconds. */ - uint64 windowSeconds; - - /** @brief Flag indicating whether an emergency pause is currently blocking auction interactions. */ - uint8 isEmergencyPaused; + uint32 numberOfInvestedProjects; }; - using GetAuctionFees_input = NoData; +protected: - struct GetAuctionFees_output + struct registerInTier_locals { - /** @brief Fee charged when a private auction is created. */ - sint64 privateAuctionFee; - - /** @brief Non-negative fee accumulated when a public auction is created and distributed at `END_EPOCH`. */ - sint64 publicAuctionCreationFee; - - /** @brief Cancellation fee rate in basis points. */ - uint64 auctionCancellationFeeBasisPoints; - - /** @brief Management fee rate in basis points. */ - uint64 managementFeeBasisPoints; - - /** @brief Development fee rate in basis points. */ - uint64 developmentFeeBasisPoints; - - /** @brief Takeover coordinator fee rate in basis points. */ - uint64 takeoverCoordinatorFeeBasisPoints; - - /** @brief Percentage of the shareholder fee distributed as dividends, in basis points. */ - uint64 shareholderDividendBasisPoints; - - /** @brief Shareholder fee tier for auctions up to the first threshold. */ - uint64 shareholderFeeBasisPointsTier1; - - /** @brief Shareholder fee tier for auctions above the first threshold and up to the second threshold. */ - uint64 shareholderFeeBasisPointsTier2; - - /** @brief Shareholder fee tier for auctions above the second threshold and up to the third threshold. */ - uint64 shareholderFeeBasisPointsTier3; - - /** @brief Shareholder fee tier for auctions above the third threshold. */ - uint64 shareholderFeeBasisPointsTier4; + uint64 tierStakedAmount; + uint32 poolWeight; }; - /** @brief Input for the arithmetic-only Batch Auction bid reward calculator. */ - struct CalculateBatchAuctionBidFee_input + PUBLIC_PROCEDURE_WITH_LOCALS(registerInTier) { - /** @brief Number of assets requested by the prospective bid. */ - uint64 bidQuantity; - - /** @brief Prospective price per asset, in qu. */ - uint64 bidAmount; - }; + if (state.get().users.contains(qpi.invocator())) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } + if (input.tierLevel < 1 || input.tierLevel > 5) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } + + switch (input.tierLevel) + { + case 1: + locals.tierStakedAmount = NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT; + locals.poolWeight = NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT; + break; + case 2: + locals.tierStakedAmount = NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT; + locals.poolWeight = NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; + break; + case 3: + locals.tierStakedAmount = NOSTROMO_TIER_DOG_STAKE_AMOUNT; + locals.poolWeight = NOSTROMO_TIER_DOG_POOL_WEIGHT; + break; + case 4: + locals.tierStakedAmount = NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT; + locals.poolWeight = NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; + break; + case 5: + locals.tierStakedAmount = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT; + locals.poolWeight = NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; + break; + default: + break; + } + if (qpi.invocationReward() < (sint64)locals.tierStakedAmount) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } + else + { + state.mut().users.set(qpi.invocator(), input.tierLevel); + state.mut().numberOfRegister++; + if (qpi.invocationReward() > (sint64)locals.tierStakedAmount) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.tierStakedAmount); + } + state.mut().totalPoolWeight += locals.poolWeight; + output.tierLevel = input.tierLevel; + } + } - /** @brief Escrow, accumulated fee, and total reward required by the Batch Auction bid arithmetic. */ - struct CalculateBatchAuctionBidFee_output + struct logoutFromTier_locals { - /** @brief Saturating product of `bidQuantity` and `bidAmount`. */ - uint64 escrowAmount; - - /** @brief Amount accumulated for distribution at `END_EPOCH` for an accepted bid: `max(100 - bidQuantity * bidAmount, 0)`. */ - uint64 fee; - - /** @brief Saturating sum of `escrowAmount` and `fee`. */ - uint64 requiredReward; + uint64 earnedAmount; + uint32 elementIndex; + uint8 tierLevel; }; - /** - * @brief Pure breakdown of one auction revenue split. - * @note Runtime settlement and tests share this struct to keep fee arithmetic aligned. - */ - struct AuctionRevenueBreakdown + PUBLIC_PROCEDURE_WITH_LOCALS(logoutFromTier) { - /** @brief Net amount that remains for the seller after every configured fee is applied. */ - uint64 sellerPayout; - - /** @brief Shareholder fee tier selected for the provided gross amount. */ - uint64 shareholderFeeBasisPoints; - - /** @brief Gross shareholder fee amount before dividend retention is split out. */ - uint64 shareholderFeeAmount; - - /** @brief Portion of the shareholder fee retained by the contract for dividend distribution. */ - uint64 shareholderDividendAmount; - - /** @brief Management wallet fee amount. */ - uint64 managementFeeAmount; - - /** @brief Development wallet fee amount. */ - uint64 developmentFeeAmount; - - /** @brief Base takeover coordinator fee charged directly from the gross amount. */ - uint64 takeoverCoordinatorBaseAmount; + if (state.get().users.contains(qpi.invocator()) == 0) + { + return ; + } + state.get().users.get(qpi.invocator(), locals.tierLevel); + switch (locals.tierLevel) + { + case 1: + locals.earnedAmount = div(NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT * NOSTROMO_TIER_FACEHUGGER_UNSTAKE_FEE, 100ULL); + qpi.transfer(qpi.invocator(), qpi.invocationReward() + NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT - locals.earnedAmount); + state.mut().epochRevenue += locals.earnedAmount; + state.mut().totalPoolWeight -= NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT; + break; + case 2: + locals.earnedAmount = div(NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT * NOSTROMO_TIER_CHESTBURST_UNSTAKE_FEE, 100ULL); + qpi.transfer(qpi.invocator(), qpi.invocationReward() + NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT - locals.earnedAmount); + state.mut().epochRevenue += locals.earnedAmount; + state.mut().totalPoolWeight -= NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; + break; + case 3: + locals.earnedAmount = div(NOSTROMO_TIER_DOG_STAKE_AMOUNT * NOSTROMO_TIER_DOG_UNSTAKE_FEE, 100ULL); + qpi.transfer(qpi.invocator(), qpi.invocationReward() + NOSTROMO_TIER_DOG_STAKE_AMOUNT - locals.earnedAmount); + state.mut().epochRevenue += locals.earnedAmount; + state.mut().totalPoolWeight -= NOSTROMO_TIER_DOG_POOL_WEIGHT; + break; + case 4: + locals.earnedAmount = div(NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT * NOSTROMO_TIER_XENOMORPH_UNSTAKE_FEE, 100ULL); + qpi.transfer(qpi.invocator(), qpi.invocationReward() + NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT - locals.earnedAmount); + state.mut().epochRevenue += locals.earnedAmount; + state.mut().totalPoolWeight -= NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; + break; + case 5: + locals.earnedAmount = div(NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT * NOSTROMO_TIER_WARRIOR_UNSTAKE_FEE, 100ULL); + qpi.transfer(qpi.invocator(), qpi.invocationReward() + NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT - locals.earnedAmount); + state.mut().epochRevenue += locals.earnedAmount; + state.mut().totalPoolWeight -= NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; + break; + default: + break; + } - /** @brief Total takeover coordinator gain including retained shareholder-fee remainder. */ - uint64 takeoverCoordinatorFeeAmount; - }; + state.mut().users.removeByKey(qpi.invocator()); + state.mut().numberOfRegister -= 1; + output.result = 1; + } - /** - * @brief Pure breakdown of one service fee charged for private-auction creation or auction cancellation. - * @note Runtime settlement and tests share this struct to keep fee arithmetic aligned. - */ - struct AuctionServiceFeeBreakdown + struct createProject_locals { - /** @brief Portion retained by the contract for shareholder dividends. */ - uint64 shareholderDividendAmount; - - /** @brief Management wallet fee amount. */ - uint64 managementFeeAmount; - - /** @brief Development wallet fee amount. */ - uint64 developmentFeeAmount; - - /** @brief Takeover coordinator wallet fee amount. */ - uint64 takeoverCoordinatorFeeAmount; + projectInfo newProject; + uint32 elementIndex, startDate, endDate, curDate; + uint8 tierLevel; }; - /** @brief Input payload used to read the wallets that receive auction fee transfers. */ - using GetFeeRecipients_input = NoData; - - struct GetFeeRecipients_output + PUBLIC_PROCEDURE_WITH_LOCALS(createProject) { - /** @brief Wallet that receives the management fee. */ - id management; - - /** @brief Wallet that receives the development fee. */ - id development; - - /** @brief Wallet that receives the takeover coordinator fee. */ - id takeoverCoordinator; - }; - - /** @brief Input payload used to read the closed auctions history ring buffer. */ - using GetClosedAuctionHistory_input = NoData; + packNostromoDate(input.startYear, input.startMonth, input.startDay, input.startHour, 0, 0, locals.startDate); + packNostromoDate(input.endYear, input.endMonth, input.endDay, input.endHour, 0, 0, locals.endDate); + packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); - struct GetClosedAuctionHistory_output - { - /** @brief Ring buffer of auction indices recorded after finalization or cancellation. */ - Array auctionIndices; - - /** @brief Total number of history writes since initialization. */ - uint64 totalEntries; - }; - - /** @brief Input payload used to read the temporary fee routing override flag. */ - using GetRouteAllFeesToDevelopment_input = NoData; - - struct GetRouteAllFeesToDevelopment_output - { - /** @brief `1` routes every fee to development, `0` uses the standard fee distribution. */ - uint8 enabled; - }; - - struct AuctionSummary - { - Array metadataIpfsCid; - id seller; - id highestBidder; - DateAndTime createdAt; - DateAndTime settledAt; - uint64 auctionIndex; - uint64 quantityForSale; - uint64 allocatedQuantity; - uint64 initialPrice; - uint64 salePrice; - uint64 buyNowPrice; - uint64 highestBidPrice; - uint64 highestBidQuantity; - uint64 highestBidAmount; - uint8 type; - uint8 visibility; - uint8 status; - }; - - struct ParticipantSummary - { - id participant; - DateAndTime lastBidTime; - uint64 bidAmount; - uint64 escrowedAmount; - uint64 requestedQuantity; - uint64 allocatedQuantity; - uint8 isWinningBid; - }; - - struct UserParticipationSummary - { - id participant; - DateAndTime lastBidTime; - uint64 auctionIndex; - uint64 bidAmount; - uint64 escrowedAmount; - uint64 requestedQuantity; - uint64 allocatedQuantity; - uint8 isWinningBid; - }; - - struct ContractStats - { - uint64 totalAuctionsCreated; - uint64 activeAuctionCount; - uint64 pendingSellerDecisionAuctionCount; - uint64 finalizedAuctionCount; - uint64 cancelledAuctionCount; - uint64 participantCount; - uint64 closedAuctionHistoryCounter; - uint64 auctionShareholderDividendPool; - uint64 pendingServiceFeePool; - uint64 totalPendingQuPayouts; - uint64 retainedClosedAuctionCount; - uint64 retainedParticipantHistoryCount; - uint32 qxTransferFee; - uint8 routeAllFeesToDevelopment; - uint8 isAuctionTimerPaused; - uint8 isPostBeginEpochPauseArmed; - uint8 isEmergencyPaused; - }; - - using GetContractStats_input = NoData; - struct GetContractStats_output - { - ContractStats stats; - }; - - struct GetAuctionSummaries_input - { - uint64 offset; - uint64 limit; - }; - struct GetAuctionSummaries_output - { - Array auctions; - uint64 totalCount; - uint64 returnedCount; - }; - - struct GetActiveAuctionIndices_input - { - uint64 offset; - uint64 limit; - }; - struct GetActiveAuctionIndices_output - { - Array auctionIndices; - uint64 totalCount; - uint64 returnedCount; - }; - - struct GetAuctionsBySeller_input - { - id seller; - uint64 offset; - uint64 limit; - }; - struct GetAuctionsBySeller_output - { - Array auctions; - uint64 totalCount; - uint64 returnedCount; - }; - - struct GetAuctionByMetadataCid_input - { - Array metadataIpfsCid; - }; - struct GetAuctionByMetadataCid_output - { - AuctionSummary auction; - uint64 auctionIndex; - uint8 found; - }; - - struct GetAuctionSummariesByIndexBatch_input - { - Array auctionIndices; - uint64 count; - }; - struct GetAuctionSummariesByIndexBatch_output - { - Array auctions; - Array found; - uint64 returnedCount; - }; - - struct GetAuctionParticipants_input - { - uint64 auctionIndex; - uint64 offset; - uint64 limit; - }; - struct GetAuctionParticipants_output - { - Array participants; - uint64 totalCount; - uint64 returnedCount; - }; - - struct GetUserParticipations_input - { - id participant; - uint64 offset; - uint64 limit; - }; - struct GetUserParticipations_output - { - Array participations; - uint64 totalCount; - uint64 returnedCount; - }; - - using GetLatestAuctionIndex_input = NoData; - struct GetLatestAuctionIndex_output - { - uint64 auctionIndex; - uint8 found; - }; - - struct GetAuctionCountBySeller_input - { - id seller; - }; - struct GetAuctionCountBySeller_output - { - uint64 count; - }; - - struct GetAuctionAtCreationSnapshot_input - { - uint64 auctionIndex; - }; - struct GetAuctionAtCreationSnapshot_output - { - id seller; - DateAndTime createdAt; - uint64 auctionIndex; - uint64 quantityForSale; - uint64 initialPrice; - uint64 salePrice; - uint64 minimumBidIncrement; - uint64 buyNowPrice; - uint64 auctionDurationSeconds; - uint8 type; - uint8 visibility; - uint8 found; - }; - - /** @brief Input payload used to read current bid capacity guidance for one active Batch Auction. */ - struct GetBatchAuctionBidAvailability_input - { - /** @brief Monotonic index of the Batch Auction to inspect. */ - uint64 auctionIndex; - }; - - /** @brief Read-only guidance for the next acceptable Batch Auction bid. */ - struct GetBatchAuctionBidAvailability_output - { - /** @brief Lowest price per asset that can currently accept a new bid meeting the auction minimum quantity. */ - uint64 minimumBidPrice; - - /** @brief Quantity available at `minimumBidPrice`; zero when no valid new bid can be accepted. */ - uint64 availableQuantity; - - /** @brief Flag indicating whether the auction exists. */ - uint8 found; - - /** @brief Flag indicating whether the auction is an active Batch Auction that can accept another valid bid. */ - uint8 isAcceptingBids; - }; - - /** @brief Internal input used to validate an auction lot and resolve its total escrow quantity. */ - struct AnalyzeAuctionLot_input - { - /** @brief Auction lot contents to validate. */ - Array auctionLotItems; - - /** @brief Requested auction duration in days. */ - uint32 durationDays; - }; - - /** @brief Internal output returned after validating an auction lot. */ - struct AnalyzeAuctionLot_output - { - /** @brief Total quantity that must be escrowed from the lot. */ - uint64 totalEscrowQuantity; - - /** @brief Number of non-empty lot entries found in the lot. */ - uint64 lotItemCount; - - /** @brief Flag indicating whether the lot and duration are valid. */ - uint8 isValid; - }; - - struct AnalyzeAuctionLot_locals - { - AuctionAssetEntry lotItem; - uint64 lotItemIndex; - }; - - /** @brief Internal input used to count non-empty wallet entries in a private wallet whitelist. */ - struct CountAllowedBidderWallets_input - { - /** @brief Wallet list provided for private wallet-based access control. */ - Array allowedBidderWallets; - }; - - /** @brief Internal output containing the number of non-empty wallet whitelist entries. */ - struct CountAllowedBidderWallets_output - { - /** @brief Number of non-zero wallet entries found in the whitelist. */ - uint64 allowedWalletCount; - }; - - struct CountAllowedBidderWallets_locals - { - uint64 allowedWalletIndex; - }; - - /** @brief Internal input used to count non-empty asset entries in a private asset access list. */ - struct CountRequiredAccessAssets_input - { - /** @brief Asset and minimum-quantity alternatives provided for private access control. */ - Array requiredAccessAssets; - }; - - /** @brief Internal output containing the number of non-empty private access assets. */ - struct CountRequiredAccessAssets_output - { - /** @brief Number of populated asset entries found in the private access list. */ - uint64 requiredAccessAssetCount; - - /** @brief Flag indicating whether every entry has a valid asset/quantity combination. */ - uint8 isValid; - }; - - struct CountRequiredAccessAssets_locals - { - AuctionAssetEntry requiredAccessAsset; - uint64 requiredAccessAssetIndex; - }; - - struct NostromoProcedureLog - { - uint32 contractIndex; - uint32 errorCode; - id actor; - sint64 amount; - uint64 auctionIndex; - uint8 procedure; - sint8 _terminator; - }; - - /** @brief Internal input used to locate either a live or retained closed auction. */ - struct FindAuction_input - { - uint64 auctionIndex; - }; - - struct FindAuction_output - { - AuctionData auction; - uint8 found; - }; - - struct FindAuction_locals - { - AuctionData archivedAuction; - uint64 historyIndex; - }; - - /** @brief Internal input used to test whether an auction remains in retained closed history. */ - struct IsClosedAuctionRetained_input - { - uint64 auctionIndex; - }; - - struct IsClosedAuctionRetained_output - { - uint8 found; - }; - - struct IsClosedAuctionRetained_locals - { - uint64 retainedClosedAuctionCount; - uint64 historyIndex; - }; - - /** @brief Internal cursor used to enumerate retained auctions in ascending creation order. */ - struct SelectNextRetainedAuction_input - { - id seller; - uint64 afterAuctionIndex; - uint8 hasAfterAuctionIndex; - uint8 includeClosedAuctions; - uint8 filterBySeller; - }; - - struct SelectNextRetainedAuction_output - { - AuctionData auction; - uint8 found; - }; - - struct SelectNextRetainedAuction_locals - { - AuctionData candidateAuction; - uint64 retainedClosedAuctionCount; - uint64 historyIndex; - sint64 auctionElementIndex; - }; - - struct CountRetainedAuctionsBySeller_input - { - id seller; - }; - - struct CountRetainedAuctionsBySeller_output - { - uint64 count; - }; - - struct CountRetainedAuctionsBySeller_locals - { - AuctionData candidateAuction; - uint64 retainedClosedAuctionCount; - uint64 historyIndex; - sint64 auctionElementIndex; - }; - - struct FindFirstRetainedAuctionByMetadataCid_input - { - Array metadataIpfsCid; - }; - - struct FindFirstRetainedAuctionByMetadataCid_output - { - AuctionData auction; - uint8 found; - }; - - struct FindFirstRetainedAuctionByMetadataCid_locals - { - AuctionData candidateAuction; - uint64 retainedClosedAuctionCount; - uint64 metadataIndex; - uint64 historyIndex; - sint64 auctionElementIndex; - uint8 metadataMatches; - }; - - struct GetAuctionByIndex_locals - { - AuctionData auction; - FindAuction_input findAuctionInput; - FindAuction_output findAuctionOutput; - AuctionAssetEntry requiredAccessAsset; - id allowedBidderWallet; - sint64 requiredAccessAssetSetIndex; - sint64 allowedBidderWalletSetIndex; - }; - - struct GetterScan_locals - { - AuctionData auction; - AuctionParticipantData participantData; - AuctionSummary auctionSummary; - ParticipantSummary participantSummary; - UserParticipationSummary userParticipationSummary; - SelectNextRetainedAuction_input selectNextAuctionInput; - SelectNextRetainedAuction_output selectNextAuctionOutput; - FindAuction_input findAuctionInput; - FindAuction_output findAuctionOutput; - uint64 auctionIndex; - uint64 boundedLimit; - uint64 metadataIndex; - uint64 requestedIndex; - uint64 participantSlotIndex; - uint64 historyIndex; - uint64 scannedAuctionCount; - sint64 auctionElementIndex; - uint8 metadataMatches; - }; - - using GetContractStats_locals = GetterScan_locals; - - struct GetAuctionSummaries_locals - { - AuctionData auction; - AuctionSummary auctionSummary; - SelectNextRetainedAuction_input selectNextAuctionInput; - SelectNextRetainedAuction_output selectNextAuctionOutput; - uint64 boundedLimit; - uint64 scannedAuctionCount; - }; - - struct GetActiveAuctionIndices_locals - { - SelectNextRetainedAuction_input selectNextAuctionInput; - SelectNextRetainedAuction_output selectNextAuctionOutput; - uint64 boundedLimit; - uint64 scannedAuctionCount; - }; - - struct GetAuctionsBySeller_locals - { - AuctionData auction; - AuctionSummary auctionSummary; - SelectNextRetainedAuction_input selectNextAuctionInput; - SelectNextRetainedAuction_output selectNextAuctionOutput; - CountRetainedAuctionsBySeller_input countAuctionsInput; - CountRetainedAuctionsBySeller_output countAuctionsOutput; - uint64 boundedLimit; - uint64 scannedAuctionCount; - }; - - struct GetAuctionByMetadataCid_locals - { - FindFirstRetainedAuctionByMetadataCid_input findAuctionInput; - FindFirstRetainedAuctionByMetadataCid_output findAuctionOutput; - }; - - using GetAuctionSummariesByIndexBatch_locals = GetterScan_locals; - using GetAuctionParticipants_locals = GetterScan_locals; - using GetUserParticipations_locals = GetterScan_locals; - - struct GetAuctionCountBySeller_locals - { - CountRetainedAuctionsBySeller_input countAuctionsInput; - CountRetainedAuctionsBySeller_output countAuctionsOutput; - }; - - using GetAuctionAtCreationSnapshot_locals = GetterScan_locals; - - struct GetAuctionParticipant_locals - { - AuctionParticipantData participantData; - uint64 participantSlotIndex; - uint64 bestParticipantSlotIndex; - uint8 bestParticipantFound; - }; - - struct GetClosedAuctionHistory_locals - { - AuctionData auction; - uint64 historyIndex; - }; - - /** @brief Internal input used to compute Batch Auction capacity at a candidate bid price. */ - struct ComputeBatchBidAvailability_input - { - /** @brief Monotonic index of the Batch Auction to inspect. */ - uint64 auctionIndex; - - /** @brief Candidate bid price; zero returns capacity at the computed minimum valid price. */ - uint64 bidAmount; - }; - - using ComputeBatchBidAvailability_output = GetBatchAuctionBidAvailability_output; - - struct ComputeBatchBidAvailability_locals - { - AuctionData auction; - AuctionParticipantData participantData; - uint64 lowestWinningPrice; - uint64 outputPrice; - uint64 priorityQuantity; - uint64 salePriorityQuantity; - uint64 effectiveCoverageQuantity; - uint64 participantIndex; - uint8 lowestWinningPriceFound; - }; - - struct GetBatchAuctionBidAvailability_locals - { - ComputeBatchBidAvailability_input computeBatchBidAvailabilityInput; - IsClosedAuctionRetained_input isClosedAuctionRetainedInput; - IsClosedAuctionRetained_output isClosedAuctionRetainedOutput; - }; - - /** @brief Internal input used to verify whether the invocator satisfies any private asset requirement. */ - struct HasRequiredAccessAsset_input - { - /** @brief Monotonic index of the auction whose private asset-based access rules should be evaluated. */ - uint64 auctionIndex; - }; - - /** @brief Internal output of the private asset access check. */ - struct HasRequiredAccessAsset_output - { - /** @brief Flag indicating whether the invocator owns the minimum quantity of any required asset. */ - uint8 hasRequiredAccessAsset; - }; - - struct HasRequiredAccessAsset_locals - { - AuctionData auction; - AuctionAssetEntry requiredAccessAsset; - sint64 requiredAccessAssetSetIndex; - sint64 possessedAccessShares; - }; - - /** @brief Internal input used to settle a batch auction after its bidding window closes. */ - struct FinalizeBatchAuction_input - { - /** @brief Timestamp used as the auction settlement time. */ - DateAndTime currentDate; - - /** @brief Monotonic index of the batch auction to finalize. */ - uint64 auctionIndex; - }; - - /** @brief Internal output returned after batch auction finalization. */ - struct FinalizeBatchAuction_output - { - /** - * @brief Flag indicating whether batch settlement finished successfully. - * @note Final allocations are never smaller than the auction minimum; any insufficient remainder is returned to the seller. - */ - uint8 success; - }; - - /** @brief Internal input used to settle a standard auction when it is accepted or auto-finalized. */ - struct FinalizeStandardAuction_input - { - /** @brief Timestamp used as the auction settlement time. */ - DateAndTime currentDate; - - /** @brief Monotonic index of the standard auction to finalize. */ - uint64 auctionIndex; - }; - - /** @brief Internal output returned after standard auction finalization. */ - struct FinalizeStandardAuction_output - { - /** @brief Flag indicating whether standard auction settlement finished successfully. */ - uint8 success; - }; - - /** @brief Internal input used to reject a pending standard auction during the seller decision window. */ - struct RejectStandardAuction_input - { - /** @brief Timestamp used as the auction settlement time. */ - DateAndTime currentDate; - - /** @brief Monotonic index of the pending standard auction to reject. */ - uint64 auctionIndex; - }; - - /** @brief Internal output returned after rejecting a pending standard auction. */ - struct RejectStandardAuction_output - { - /** @brief Amount refunded to the highest bidder after the rejection. */ - uint64 refundedAmount; - - /** @brief Flag indicating whether the rejection flow finished successfully. */ - uint8 success; - }; - - /** @brief Internal input used to evaluate whether auction interactions are currently paused. */ - struct IsAuctionInteractionPaused_input - { - }; - - /** @brief Internal output of the auction interaction pause check. */ - struct IsAuctionInteractionPaused_output - { - /** @brief Flag indicating whether auction interactions are blocked by bootstrap time or by epoch timing pauses. */ - uint8 isPaused; - }; - - /** @brief Internal input used to resolve the currently active global auction pause interval. */ - struct GetAuctionPauseState_input - { - }; - - /** @brief Internal output describing the current global auction pause interval. */ - struct GetAuctionPauseState_output - { - /** @brief Pause start timestamp for the current active pause interval. */ - DateAndTime pauseStartedAt; - - /** @brief Pause end timestamp for the current active pause interval. */ - DateAndTime pauseEndsAt; - - /** @brief Flag indicating whether auction timers are currently paused. */ - uint8 isPaused; - }; - - /** @brief Internal locals used to resolve the currently active global auction pause interval. */ - struct GetAuctionPauseState_locals - { - /** @brief Compact current date marker used to detect the bootstrap default time sentinel. */ - DateAndTime currentDate; - uint32 currentDateStamp; - }; - - using SyncAuctionPauseState_input = NoData; - using SyncAuctionPauseState_output = NoData; - - /** @brief Internal locals used to synchronize auction deadlines with the global pause interval. */ - struct SyncAuctionPauseState_locals - { - AuctionData auction; - DateAndTime currentDate; - GetAuctionPauseState_input getAuctionPauseStateInput; - GetAuctionPauseState_output getAuctionPauseStateOutput; - uint64 pausedSeconds; - sint64 auctionIndex; - }; - - /** @brief Internal input used to split auction proceeds between seller and configured fee recipients. */ - struct DistributeAuctionRevenue_input - { - /** @brief Seller wallet that receives the net proceeds. */ - id seller; - - /** @brief Gross amount collected from the auction before fee distribution. */ - uint64 grossAmount; - }; - - /** @brief Internal output returned after auction revenue distribution is computed. */ - struct DistributeAuctionRevenue_output - { - /** @brief Net amount that should be transferred to the seller after auction fees. */ - uint64 sellerPayout; - - /** @brief Flag indicating whether the revenue distribution completed successfully. */ - uint8 success; - }; - - /** @brief Internal input used to accrue a service fee in the shared Nostromo fee pool. */ - struct AccumulateAuctionServiceFee_input - { - /** @brief Fee amount that should be accumulated. */ - uint64 feeAmount; - }; - - /** @brief Internal output returned after service-fee accrual is completed. */ - struct AccumulateAuctionServiceFee_output - { - /** @brief Flag indicating whether the service fee was recorded. */ - uint8 success; - }; - - using DistributeNostromoFeePool_input = NoData; - - struct DistributeNostromoFeePool_output - { - /** @brief Flag indicating whether every current pool accumulator was durably settled. */ - uint8 success; - }; - - /** @brief Internal input used to compute the remaining post-BEGIN_EPOCH launch pause. */ - struct GetTicksBeforeAuctionLaunchInternal_input - { - }; - - /** @brief Internal output containing the remaining post-BEGIN_EPOCH launch pause. */ - struct GetTicksBeforeAuctionLaunchInternal_output - { - /** @brief Number of ticks remaining before auction interactions resume after `BEGIN_EPOCH`. */ - uint32 ticks; - }; - - struct GetTicksBeforeAuctionLaunchInternal_locals - { - DateAndTime currentDate; - DateAndTime pauseEndsAt; - uint64 remainingSeconds; - }; - - struct GetTicksBeforeAuctionLaunch_locals - { - DateAndTime currentDate; - DateAndTime pauseEndsAt; - uint64 remainingSeconds; - }; - - /** @brief Internal input used to register a QU liability before settlement side effects are committed. */ - struct QueueQuPayout_input - { - id recipient; - uint64 amount; - }; - - struct QueueQuPayout_output - { - uint8 success; - }; - - struct QueueQuPayout_locals - { - uint64 previousAmount; - uint64 updatedAmount; - sint64 payoutIndex; - }; - - /** @brief Internal input used to discharge a bounded number of QPI-sized payout chunks. */ - struct FlushQuPayout_input - { - id recipient; - uint64 maxChunks; - }; - - struct FlushQuPayout_output - { - uint64 transferredAmount; - uint64 remainingAmount; - uint8 success; - }; - - struct FlushQuPayout_locals - { - uint64 chunkAmount; - uint64 chunkIndex; - sint64 transferResult; - }; - - using ProcessPendingQuPayouts_input = NoData; - using ProcessPendingQuPayouts_output = NoData; - - /** @brief Internal locals used by the bounded round-robin pending-payout processor. */ - struct ProcessPendingQuPayouts_locals - { - FlushQuPayout_input flushQuPayoutInput; - FlushQuPayout_output flushQuPayoutOutput; - id pendingPayoutRecipient; - uint64 payoutScanIndex; - uint64 payoutTargetRecipientCount; - uint64 processedPayoutRecipientCount; - sint64 payoutElementIndex; - }; - - struct QueueAndFlushQuPayout_input - { - id recipient; - uint64 amount; - uint64 maxChunks; - }; - - struct QueueAndFlushQuPayout_output - { - uint64 transferredAmount; - uint64 remainingAmount; - uint8 success; - }; - - struct QueueAndFlushQuPayout_locals - { - QueueQuPayout_input queueQuPayoutInput; - QueueQuPayout_output queueQuPayoutOutput; - FlushQuPayout_input flushQuPayoutInput; - FlushQuPayout_output flushQuPayoutOutput; - }; - - /** @brief Internal input used to move a bid record from live storage into bounded history. */ - struct ArchiveParticipant_input - { - AuctionParticipantData participantData; - }; - - using ArchiveParticipant_output = NoData; - - struct ArchiveParticipant_locals - { - uint64 historyIndex; - }; - - /** @brief Internal input used to process a batch auction bid after the common PlaceBid checks succeed. */ - struct ProcessBatchBid_input - { - /** @brief Monotonic index of the target batch auction. */ - uint64 auctionIndex; - - /** @brief Quantity requested by the bidder in the batch auction. */ - uint64 effectiveQuantity; - - /** @brief Offered price per asset for the requested quantity in the batch auction. */ - uint64 bidAmount; - - /** @brief Timestamp of the accepted bid. */ - DateAndTime currentDate; - - /** @brief Seconds elapsed since auction creation at the moment of the bid. */ - uint64 elapsedSeconds; - }; - - /** @brief Internal input used to refresh the cached highest bid fields of one batch auction. */ - struct RecomputeBatchHighestBid_input - { - /** @brief Monotonic index of the batch auction whose cached top bid must be rebuilt. */ - uint64 auctionIndex; - }; - - using RecomputeBatchHighestBid_output = NoData; - - /** @brief Internal output returned after processing a batch auction bid. */ - struct ProcessBatchBid_output - { - /** @brief Amount that remains escrowed for the accepted batch bid. */ - uint64 escrowedAmount; - - /** @brief Amount refunded during batch bid processing. */ - uint64 refundedAmount; - - /** @brief Result code describing whether the batch bid processing succeeded. */ - EAuctionError errorCode; - - /** @brief Flag indicating whether batch bid processing completed successfully. */ - uint8 success; - }; - - struct ProcessBatchBid_locals - { - AuctionData auction; - AuctionParticipantData participantData; - AuctionParticipantData worstParticipantData; - ComputeBatchBidAvailability_input computeBatchBidAvailabilityInput; - ComputeBatchBidAvailability_output computeBatchBidAvailabilityOutput; - RecomputeBatchHighestBid_input recomputeBatchHighestBidInput; - RecomputeBatchHighestBid_output recomputeBatchHighestBidOutput; - ArchiveParticipant_input archiveParticipantInput; - ArchiveParticipant_output archiveParticipantOutput; - QueueAndFlushQuPayout_input payoutInput; - QueueAndFlushQuPayout_output payoutOutput; - AccumulateAuctionServiceFee_input accumulateAuctionServiceFeeInput; - AccumulateAuctionServiceFee_output accumulateAuctionServiceFeeOutput; - uint64 activeQuantity; - uint64 displacedQuantity; - uint64 displacedRefund; - uint64 excessQuantity; - uint64 remainingWorstQuantity; - CalculateBatchAuctionBidFee_output bidFeeCalculation; - uint64 participantIndex; - uint64 freeParticipantSlotIndex; - uint64 worstParticipantSlotIndex; - uint8 worstParticipantFound; - uint8 freeParticipantSlotFound; - }; - - struct RecomputeBatchHighestBid_locals - { - AuctionData auction; - AuctionParticipantData participantData; - AuctionParticipantData bestParticipantData; - uint64 participantIndex; - uint64 bestParticipantSlotIndex; - uint8 bestParticipantFound; - }; - - /** @brief Internal input used to process a standard auction bid after the common PlaceBid checks succeed. */ - struct ProcessStandardBid_input - { - /** @brief Monotonic index of the target standard auction. */ - uint64 auctionIndex; - - /** @brief Total amount the bidder commits for the standard auction lot. */ - uint64 bidAmount; - - /** @brief Timestamp of the accepted bid. */ - DateAndTime currentDate; - - /** @brief Seconds elapsed since auction creation at the moment of the bid. */ - uint64 elapsedSeconds; - }; - - /** @brief Internal output returned after processing a standard auction bid. */ - struct ProcessStandardBid_output - { - /** @brief Amount that remains escrowed for the accepted standard bid. */ - uint64 escrowedAmount; - - /** @brief Amount refunded during standard bid processing. */ - uint64 refundedAmount; - - /** @brief Result code describing whether the standard bid processing succeeded. */ - EAuctionError errorCode; - - /** @brief Flag indicating whether standard bid processing completed successfully. */ - uint8 success; - }; - - struct ProcessStandardBid_locals - { - AuctionData auction; - AuctionParticipantData participantData; - AuctionParticipantData previousHighestBidderData; - FinalizeStandardAuction_input finalizeStandardAuctionInput; - FinalizeStandardAuction_output finalizeStandardAuctionOutput; - ArchiveParticipant_input archiveParticipantInput; - ArchiveParticipant_output archiveParticipantOutput; - QueueAndFlushQuPayout_input payoutInput; - QueueAndFlushQuPayout_output payoutOutput; - uint64 previousEscrow; - uint64 requiredEscrow; - uint64 participantSlotIndex; - uint64 highestBidderSlotIndex; - uint64 freeParticipantSlotIndex; - uint8 participantExists; - uint8 highestBidderExists; - uint8 freeParticipantSlotFound; - uint8 finalizeImmediately; - }; - - /** @brief Internal input used to validate the IPFS metadata CID format required by the Auction House. */ - struct ValidateMetadataCid_input - { - /** @brief Candidate lowercase base32 CIDv1 for auction metadata stored in Pinata. */ - Array metadataIpfsCid; - }; - - /** @brief Internal output of the metadata CID validation routine. */ - struct ValidateMetadataCid_output - { - /** @brief Flag indicating whether the metadata CID has the required lowercase base32 CIDv1 format. */ - uint8 isValid; - }; - - struct ValidateMetadataCid_locals - { - uint64 cidIndex; - uint8 cidChar; - uint8 hasPayloadCharacters; - uint8 reachedTerminator; - }; - - /** @brief Internal input used to verify that the seller owns enough shares for every asset in the auction lot. */ - struct VerifyAuctionLotBalances_input - { - /** @brief Auction lot that should be checked against the seller balance. */ - Array auctionLotItems; - }; - - /** @brief Internal output of the seller balance verification routine. */ - struct VerifyAuctionLotBalances_output - { - /** @brief Flag indicating whether the seller owns enough shares for the entire lot. */ - uint8 hasEnoughBalance; - }; - - struct VerifyAuctionLotBalances_locals - { - AuctionAssetEntry lotItem; - uint64 lotItemIndex; - sint64 possessedShares; - }; - - /** @brief Internal input used to transfer the auction lot from the seller into contract escrow. */ - struct EscrowAuctionLotAssets_input - { - /** @brief Auction lot that must be moved into contract escrow. */ - Array auctionLotItems; - }; - - /** @brief Internal output of the lot escrow routine. */ - struct EscrowAuctionLotAssets_output - { - /** @brief Flag indicating whether every lot asset was successfully escrowed. */ - uint8 success; - }; - - struct EscrowAuctionLotAssets_locals - { - AuctionAssetEntry lotItem; - uint64 lotItemIndex; - uint64 rollbackLotItemIndex; - sint64 remainingShares; - }; - - /** @brief Internal input used to return an auction lot from contract escrow to a target wallet. */ - struct RollbackAuctionLotAssets_input - { - /** @brief Auction lot that must be transferred out of contract escrow. */ - Array auctionLotItems; - - /** @brief Destination wallet that should receive the lot from escrow. */ - id recipient; - }; - - using RollbackAuctionLotAssets_output = NoData; - - struct RollbackAuctionLotAssets_locals - { - AuctionAssetEntry lotItem; - uint64 lotItemIndex; - }; - - /** @brief Internal input used to archive and remove a closed auction from active storage. */ - struct ArchiveClosedAuction_input - { - AuctionData auction; - }; - - using ArchiveClosedAuction_output = NoData; - - struct ArchiveClosedAuction_locals - { - uint64 historyIndex; - }; - - struct FinalizeBatchAuction_locals - { - AuctionData auction; - AuctionParticipantData participantData; - AuctionParticipantData bestParticipantData; - AuctionAssetEntry batchLotItem; - DistributeAuctionRevenue_input distributeAuctionRevenueInput; - DistributeAuctionRevenue_output distributeAuctionRevenueOutput; - ArchiveParticipant_input archiveParticipantInput; - ArchiveParticipant_output archiveParticipantOutput; - ArchiveClosedAuction_input archiveClosedAuctionInput; - ArchiveClosedAuction_output archiveClosedAuctionOutput; - QueueAndFlushQuPayout_input payoutInput; - QueueAndFlushQuPayout_output payoutOutput; - DateAndTime currentDate; - uint64 remainingQuantity; - uint64 allocatedQuantity; - uint64 requiredPayment; - uint64 refundAmount; - uint64 soldQuantity; - uint64 totalGrossAmount; - uint64 lotItemIndex; - uint64 participantIndex; - uint64 bestParticipantSlotIndex; - uint8 bestParticipantFound; - uint8 lotItemFound; - }; - - struct FinalizeStandardAuction_locals - { - AuctionData auction; - AuctionParticipantData highestBidderData; - RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; - RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; - DistributeAuctionRevenue_input distributeAuctionRevenueInput; - DistributeAuctionRevenue_output distributeAuctionRevenueOutput; - ArchiveParticipant_input archiveParticipantInput; - ArchiveParticipant_output archiveParticipantOutput; - ArchiveClosedAuction_input archiveClosedAuctionInput; - ArchiveClosedAuction_output archiveClosedAuctionOutput; - QueueAndFlushQuPayout_input payoutInput; - QueueAndFlushQuPayout_output payoutOutput; - uint64 highestBidderSlotIndex; - uint8 highestBidderExists; - uint8 lotSold; - }; - - struct DistributeAuctionRevenue_locals - { - AuctionRevenueBreakdown auctionRevenueBreakdown; - NostromoFeePool feePool; - QueueAndFlushQuPayout_input payoutInput; - QueueAndFlushQuPayout_output payoutOutput; - uint64 shareholderFeeTierIndex; - }; - - struct AccumulateAuctionServiceFee_locals - { - NostromoFeePool feePool; - }; - - struct DistributeNostromoFeePool_locals - { - AuctionServiceFeeBreakdown auctionServiceFeeBreakdown; - NostromoFeePool feePool; - QueueAndFlushQuPayout_input payoutInput; - QueueAndFlushQuPayout_output payoutOutput; - uint64 shareholderDividendAmount; - uint64 distributedDividendAmount; - uint64 dividendPerShare; - }; - - struct RejectStandardAuction_locals - { - AuctionData auction; - AuctionParticipantData highestBidderData; - RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; - RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; - ArchiveParticipant_input archiveParticipantInput; - ArchiveParticipant_output archiveParticipantOutput; - ArchiveClosedAuction_input archiveClosedAuctionInput; - ArchiveClosedAuction_output archiveClosedAuctionOutput; - QueueAndFlushQuPayout_input payoutInput; - QueueAndFlushQuPayout_output payoutOutput; - uint64 highestBidderSlotIndex; - uint8 highestBidderExists; - }; - - struct CreateAuction_locals - { - AuctionData auction; - NostromoProcedureLog log; - IsAuctionInteractionPaused_input isAuctionInteractionPausedInput; - IsAuctionInteractionPaused_output isAuctionInteractionPausedOutput; - ValidateMetadataCid_input validateMetadataCidInput; - ValidateMetadataCid_output validateMetadataCidOutput; - AnalyzeAuctionLot_input analyzeAuctionLotInput; - AnalyzeAuctionLot_output analyzeAuctionLotOutput; - CountAllowedBidderWallets_input countAllowedBidderWalletsInput; - CountAllowedBidderWallets_output countAllowedBidderWalletsOutput; - CountRequiredAccessAssets_input countRequiredAccessAssetsInput; - CountRequiredAccessAssets_output countRequiredAccessAssetsOutput; - AuctionAssetEntry requiredAccessAsset; - VerifyAuctionLotBalances_input verifyAuctionLotBalancesInput; - EscrowAuctionLotAssets_input escrowAuctionLotAssetsInput; - RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; - AccumulateAuctionServiceFee_input accumulateAuctionServiceFeeInput; - sint64 requiredFee; - sint64 existingRequiredAccessQuantity; - uint64 resolvedQuantityForSale; - uint64 resolvedMinimumPurchaseQuantity; - uint64 allowedWalletIndex; - uint64 requiredAccessAssetIndex; - RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; - EscrowAuctionLotAssets_output escrowAuctionLotAssetsOutput; - VerifyAuctionLotBalances_output verifyAuctionLotBalancesOutput; - AccumulateAuctionServiceFee_output accumulateAuctionServiceFeeOutput; - }; - - struct PlaceBid_locals - { - AuctionData auction; - FindAuction_input findAuctionInput; - FindAuction_output findAuctionOutput; - NostromoProcedureLog log; - IsAuctionInteractionPaused_input isAuctionInteractionPausedInput; - IsAuctionInteractionPaused_output isAuctionInteractionPausedOutput; - HasRequiredAccessAsset_input hasRequiredAccessAssetInput; - HasRequiredAccessAsset_output hasRequiredAccessAssetOutput; - ProcessBatchBid_input processBatchBidInput; - ProcessBatchBid_output processBatchBidOutput; - ProcessStandardBid_input processStandardBidInput; - ProcessStandardBid_output processStandardBidOutput; - uint64 elapsedSeconds; - DateAndTime currentDate; - uint8 hasAccess; - }; - - struct CancelAuction_locals - { - AuctionData auction; - FindAuction_input findAuctionInput; - FindAuction_output findAuctionOutput; - AuctionParticipantData participantData; - NostromoProcedureLog log; - RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; - RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; - AccumulateAuctionServiceFee_input accumulateAuctionServiceFeeInput; - AccumulateAuctionServiceFee_output accumulateAuctionServiceFeeOutput; - ArchiveClosedAuction_input archiveClosedAuctionInput; - ArchiveClosedAuction_output archiveClosedAuctionOutput; - DateAndTime currentDate; - uint64 cancellationBaseAmount; - uint64 participantIndex; - }; - - struct ResolvePendingStandardAuction_locals - { - AuctionData auction; - FindAuction_input findAuctionInput; - FindAuction_output findAuctionOutput; - DateAndTime currentDate; - NostromoProcedureLog log; - - IsAuctionInteractionPaused_input isAuctionInteractionPausedInput; - IsAuctionInteractionPaused_output isAuctionInteractionPausedOutput; - FinalizeStandardAuction_input finalizeStandardAuctionInput; - FinalizeStandardAuction_output finalizeStandardAuctionOutput; - RejectStandardAuction_input rejectStandardAuctionInput; - RejectStandardAuction_output rejectStandardAuctionOutput; - }; - - struct END_TICK_locals - { - AuctionData auction; - DateAndTime currentDate; - SyncAuctionPauseState_input syncAuctionPauseStateInput; - SyncAuctionPauseState_output syncAuctionPauseStateOutput; - uint64 elapsedSeconds; - uint32 currentDateStamp; - sint64 auctionIndex; - FinalizeBatchAuction_input finalizeBatchAuctionInput; - FinalizeBatchAuction_output finalizeBatchAuctionOutput; - FinalizeStandardAuction_input finalizeStandardAuctionInput; - FinalizeStandardAuction_output finalizeStandardAuctionOutput; - sint64 currentReserve; - sint64 reserveDrop; - uint64 guardElapsedSeconds; - uint64 guardDropThreshold; - }; - - struct BEGIN_EPOCH_locals - { - QX::Fees_input feesInput; - QX::Fees_output feesOutput; - }; - - struct END_EPOCH_locals - { - DistributeNostromoFeePool_input distributeNostromoFeePoolInput; - DistributeNostromoFeePool_output distributeNostromoFeePoolOutput; - ProcessPendingQuPayouts_input processPendingQuPayoutsInput; - ProcessPendingQuPayouts_output processPendingQuPayoutsOutput; - }; - - /** @brief Input payload used to move share management rights to another managing contract. */ - struct TransferShareManagementRights_input - { - /** @brief Asset whose management rights should be transferred. */ - Asset asset; - - /** @brief Number of shares whose management rights should be transferred. */ - sint64 numberOfShares; - - /** @brief Destination managing contract index. */ - uint32 newManagingContractIndex; - }; - - /** @brief Result of a share management rights transfer request. */ - struct TransferShareManagementRights_output - { - /** @brief Number of shares whose management rights were transferred. */ - sint64 transferredNumberOfShares; - - /** @brief Result code describing whether the transfer request succeeded. */ - EAuctionError errorCode; - }; - - struct TransferShareManagementRights_locals - { - NostromoProcedureLog log; - - sint64 result; - sint64 reward; - sint64 refundAmount; - bit success; - }; - - struct SetAuctionFees_locals - { - NostromoProcedureLog log; - }; - - struct SetAuctionFeesByManagement_locals - { - NostromoProcedureLog log; - }; - - struct SetManagement_locals - { - NostromoProcedureLog log; - }; - - struct SetFeeReserveGuardConfig_locals - { - NostromoProcedureLog log; - }; - - struct SetEmergencyPause_locals - { - NostromoProcedureLog log; - }; - - REGISTER_USER_FUNCTIONS_AND_PROCEDURES() - { - REGISTER_USER_PROCEDURE(CreateAuction, static_cast(EProcedureId::CreateAuction)); - REGISTER_USER_PROCEDURE(PlaceBid, static_cast(EProcedureId::PlaceBid)); - REGISTER_USER_PROCEDURE(CancelAuction, static_cast(EProcedureId::CancelAuction)); - REGISTER_USER_PROCEDURE(TransferShareManagementRights, static_cast(EProcedureId::TransferShareManagementRights)); - REGISTER_USER_PROCEDURE(ResolvePendingStandardAuction, static_cast(EProcedureId::ResolvePendingStandardAuction)); - REGISTER_USER_PROCEDURE(SetAuctionFees, static_cast(EProcedureId::SetAuctionFees)); - REGISTER_USER_PROCEDURE(SetAuctionFeesByManagement, static_cast(EProcedureId::SetAuctionFeesByManagement)); - REGISTER_USER_PROCEDURE(SetManagement, static_cast(EProcedureId::SetManagement)); - REGISTER_USER_PROCEDURE(SetFeeReserveGuardConfig, static_cast(EProcedureId::SetFeeReserveGuardConfig)); - REGISTER_USER_PROCEDURE(SetEmergencyPause, static_cast(EProcedureId::SetEmergencyPause)); - - REGISTER_USER_FUNCTION(GetAuctionByIndex, static_cast(EFunctionId::GetAuctionByIndex)); - REGISTER_USER_FUNCTION(GetAuctionParticipant, static_cast(EFunctionId::GetAuctionParticipant)); - REGISTER_USER_FUNCTION(GetTicksBeforeAuctionLaunch, static_cast(EFunctionId::GetTicksBeforeAuctionLaunch)); - REGISTER_USER_FUNCTION(GetAuctionFees, static_cast(EFunctionId::GetAuctionFees)); - REGISTER_USER_FUNCTION(GetFeeRecipients, static_cast(EFunctionId::GetFeeRecipients)); - REGISTER_USER_FUNCTION(GetClosedAuctionHistory, static_cast(EFunctionId::GetClosedAuctionHistory)); - REGISTER_USER_FUNCTION(GetRouteAllFeesToDevelopment, static_cast(EFunctionId::GetRouteAllFeesToDevelopment)); - REGISTER_USER_FUNCTION(GetContractStats, static_cast(EFunctionId::GetContractStats)); - REGISTER_USER_FUNCTION(GetAuctionSummaries, static_cast(EFunctionId::GetAuctionSummaries)); - REGISTER_USER_FUNCTION(GetActiveAuctionIndices, static_cast(EFunctionId::GetActiveAuctionIndices)); - REGISTER_USER_FUNCTION(GetAuctionsBySeller, static_cast(EFunctionId::GetAuctionsBySeller)); - REGISTER_USER_FUNCTION(GetAuctionByMetadataCid, static_cast(EFunctionId::GetAuctionByMetadataCid)); - REGISTER_USER_FUNCTION(GetAuctionSummariesByIndexBatch, static_cast(EFunctionId::GetAuctionSummariesByIndexBatch)); - REGISTER_USER_FUNCTION(GetAuctionParticipants, static_cast(EFunctionId::GetAuctionParticipants)); - REGISTER_USER_FUNCTION(GetUserParticipations, static_cast(EFunctionId::GetUserParticipations)); - REGISTER_USER_FUNCTION(GetLatestAuctionIndex, static_cast(EFunctionId::GetLatestAuctionIndex)); - REGISTER_USER_FUNCTION(GetAuctionCountBySeller, static_cast(EFunctionId::GetAuctionCountBySeller)); - REGISTER_USER_FUNCTION(GetAuctionAtCreationSnapshot, static_cast(EFunctionId::GetAuctionAtCreationSnapshot)); - REGISTER_USER_FUNCTION(GetBatchAuctionBidAvailability, static_cast(EFunctionId::GetBatchAuctionBidAvailability)); - REGISTER_USER_FUNCTION(CalculateBatchAuctionBidFee, static_cast(EFunctionId::CalculateBatchAuctionBidFee)); - REGISTER_USER_FUNCTION(GetPendingServiceFeePool, static_cast(EFunctionId::GetPendingServiceFeePool)); - REGISTER_USER_FUNCTION(GetFeeReserveGuardState, static_cast(EFunctionId::GetFeeReserveGuardState)); - REGISTER_USER_FUNCTION(GetPendingPayout, static_cast(EFunctionId::GetPendingPayout)); - REGISTER_USER_FUNCTION(GetNostromoFeePool, static_cast(EFunctionId::GetNostromoFeePool)); - } - - /** - * @brief Initializes default governance, fee, pause, and guard settings. - */ - INITIALIZE() - { - // Install the default governance, fee, pause, and guard configuration into the zeroed contract state. - state.mut().privateAuctionFee = NOST_DEFAULT_PRIVATE_AUCTION_FEE; - state.mut().publicAuctionCreationFee = NOST_PUBLIC_AUCTION_CREATION_FEE; - state.mut().auctionCancellationFeeBasisPoints = NOST_DEFAULT_AUCTION_CANCELLATION_FEE_BP; - state.mut().managementFeeBasisPoints = NOST_DEFAULT_AUCTION_MANAGEMENT_FEE_BP; - state.mut().developmentFeeBasisPoints = NOST_DEFAULT_AUCTION_DEVELOPMENT_FEE_BP; - state.mut().takeoverCoordinatorFeeBasisPoints = NOST_DEFAULT_AUCTION_TAKEOVER_COORDINATOR_FEE_BP; - state.mut().shareholderDividendBasisPoints = NOST_DEFAULT_AUCTION_SHAREHOLDER_DIVIDEND_BP; - state.mut().shareholderFeeBasisPointsTier1 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_1; - state.mut().shareholderFeeBasisPointsTier2 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_2; - state.mut().shareholderFeeBasisPointsTier3 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_3; - state.mut().shareholderFeeBasisPointsTier4 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4; - state.mut().maxAuctionDurationDays = NOST_AUCTION_MAX_DURATION_DAYS; - state.mut().isAuctionTimerPaused = 1; - state.mut().routeAllFeesToDevelopment = NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT; - state.mut().auctionTimerPauseStartedAt.setInvalid(); - state.mut().auctionTimerPauseEndsAt.setInvalid(); - state.mut().feeReserveGuardDropBasisPoints = NOST_DEFAULT_FEE_RESERVE_GUARD_DROP_BP; - state.mut().feeReserveGuardWindowSeconds = NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS; - state.mut().management = ID(_I, _G, _P, _Z, _X, _Q, _O, _R, _J, _Y, _Q, _P, _A, _G, _V, _A, _B, _N, _T, _N, _I, _S, _O, _Y, _T, _M, _T, _A, - _N, _M, _K, _Z, _A, _S, _T, _P, _P, _G, _Z, _O, _N, _A, _Q, _J, _X, _Q, _O, _S, _W, _Q, _O, _V, _J, _C, _K, _D); - state.mut().development = ID(_D, _Q, _V, _H, _M, _Z, _F, _C, _W, _O, _K, _M, _H, _F, _B, _H, _L, _X, _U, _I, _U, _G, _P, _P, _X, _R, _Z, _C, - _U, _V, _S, _N, _J, _F, _Z, _J, _F, _M, _Q, _M, _Y, _D, _B, _X, _E, _S, _E, _A, _T, _M, _W, _L, _K, _N, _L, _D); - state.mut().takeoverCoordinator = - ID(_X, _J, _O, _S, _N, _L, _T, _Z, _V, _V, _H, _N, _Z, _C, _B, _Y, _X, _I, _E, _V, _N, _E, _P, _P, _B, _O, _Q, _A, _W, _D, _B, _V, _G, _E, - _N, _Z, _O, _X, _S, _V, _O, _B, _K, _G, _Z, _C, _C, _F, _D, _B, _D, _M, _T, _M, _L, _C); - } - - MIGRATE() - { - state.mut().privateAuctionFee = NOST_DEFAULT_PRIVATE_AUCTION_FEE; - state.mut().publicAuctionCreationFee = NOST_PUBLIC_AUCTION_CREATION_FEE; - state.mut().auctionCancellationFeeBasisPoints = NOST_DEFAULT_AUCTION_CANCELLATION_FEE_BP; - state.mut().managementFeeBasisPoints = NOST_DEFAULT_AUCTION_MANAGEMENT_FEE_BP; - state.mut().developmentFeeBasisPoints = NOST_DEFAULT_AUCTION_DEVELOPMENT_FEE_BP; - state.mut().takeoverCoordinatorFeeBasisPoints = NOST_DEFAULT_AUCTION_TAKEOVER_COORDINATOR_FEE_BP; - state.mut().shareholderDividendBasisPoints = NOST_DEFAULT_AUCTION_SHAREHOLDER_DIVIDEND_BP; - state.mut().shareholderFeeBasisPointsTier1 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_1; - state.mut().shareholderFeeBasisPointsTier2 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_2; - state.mut().shareholderFeeBasisPointsTier3 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_3; - state.mut().shareholderFeeBasisPointsTier4 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4; - state.mut().maxAuctionDurationDays = NOST_AUCTION_MAX_DURATION_DAYS; - state.mut().routeAllFeesToDevelopment = NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT; - state.mut().feeReserveGuardDropBasisPoints = NOST_DEFAULT_FEE_RESERVE_GUARD_DROP_BP; - state.mut().feeReserveGuardWindowSeconds = NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS; - state.mut().management = ID(_I, _G, _P, _Z, _X, _Q, _O, _R, _J, _Y, _Q, _P, _A, _G, _V, _A, _B, _N, _T, _N, _I, _S, _O, _Y, _T, _M, _T, _A, - _N, _M, _K, _Z, _A, _S, _T, _P, _P, _G, _Z, _O, _N, _A, _Q, _J, _X, _Q, _O, _S, _W, _Q, _O, _V, _J, _C, _K, _D); - state.mut().development = ID(_D, _Q, _V, _H, _M, _Z, _F, _C, _W, _O, _K, _M, _H, _F, _B, _H, _L, _X, _U, _I, _U, _G, _P, _P, _X, _R, _Z, _C, - _U, _V, _S, _N, _J, _F, _Z, _J, _F, _M, _Q, _M, _Y, _D, _B, _X, _E, _S, _E, _A, _T, _M, _W, _L, _K, _N, _L, _D); - state.mut().takeoverCoordinator = - ID(_X, _J, _O, _S, _N, _L, _T, _Z, _V, _V, _H, _N, _Z, _C, _B, _Y, _X, _I, _E, _V, _N, _E, _P, _P, _B, _O, _Q, _A, _W, _D, _B, _V, _G, _E, - _N, _Z, _O, _X, _S, _V, _O, _B, _K, _G, _Z, _C, _C, _F, _D, _B, _D, _M, _T, _M, _L, _C); - } - - /** - * @brief Allows share acquisition without charging an additional contract fee. - */ - PRE_ACQUIRE_SHARES() - { - output.requestedFee = 0; - output.allowTransfer = true; - } - - /** - * @brief Refreshes epoch-scoped configuration and arms auction timer pauses. - */ - BEGIN_EPOCH_WITH_LOCALS() - { - // Refresh the QX fee cache once per epoch so share transfers can expose current cost guidance. - CALL_OTHER_CONTRACT_FUNCTION(QX, Fees, locals.feesInput, locals.feesOutput); - // Preserve the previous cache when QX is temporarily unavailable; a failed call must not install an undefined fee. - if (interContractCallError == NoCallError) - { - state.mut().qxTransferFee = locals.feesOutput.transferFee; - } - - // Freeze auction timers across the epoch boundary; END_TICK later accounts this pause back into deadlines. - state.mut().isPostBeginEpochPauseArmed = 1; - if (!state.get().isAuctionTimerPaused) - { - state.mut().isAuctionTimerPaused = 1; - state.mut().auctionTimerPauseStartedAt = qpi.now(); - state.mut().auctionTimerPauseEndsAt = qpi.now(); - return; - } - - if (!state.get().auctionTimerPauseStartedAt.isValid() || qpi.now() < state.get().auctionTimerPauseStartedAt) - { - state.mut().auctionTimerPauseStartedAt = qpi.now(); - } - if (!state.get().auctionTimerPauseEndsAt.isValid() || qpi.now() > state.get().auctionTimerPauseEndsAt) - { - state.mut().auctionTimerPauseEndsAt = qpi.now(); - } - } - - /** - * @brief Retries pending QU payouts, settles the shared Nostromo fee pool, and performs storage cleanup. - */ - END_EPOCH_WITH_LOCALS() - { - CALL(ProcessPendingQuPayouts, locals.processPendingQuPayoutsInput, locals.processPendingQuPayoutsOutput); - - CALL(DistributeNostromoFeePool, locals.distributeNostromoFeePoolInput, locals.distributeNostromoFeePoolOutput); - - state.mut().auctionList.cleanupIfNeeded(); - state.mut().pendingQuPayouts.cleanupIfNeeded(); - } - - /** - * @brief Advances auction lifecycle state and finalizes auctions whose deadlines elapsed. - */ - END_TICK_WITH_LOCALS() - { - makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentDateStamp); - locals.currentDate = qpi.now(); - - // The reserve guard converts a sudden execution-fee reserve drop into an emergency pause. - if (!state.get().isEmergencyPaused) - { - locals.currentReserve = qpi.queryFeeReserve(SELF_INDEX); - // The first observation establishes a baseline instead of interpreting startup state as a reserve drop. - if (!state.get().feeReserveBaselineAt.isValid()) - { - state.mut().feeReserveBaseline = locals.currentReserve; - state.mut().feeReserveBaselineAt = locals.currentDate; - } - else - { - // Subsequent observations either trigger the guard or roll the baseline into a new window. - diffDateInSecond(state.get().feeReserveBaselineAt, locals.currentDate, locals.guardElapsedSeconds); - locals.reserveDrop = state.get().feeReserveBaseline - locals.currentReserve; - if (state.get().feeReserveBaseline > 0 && locals.reserveDrop > 0) - { - locals.guardDropThreshold = - div(smul(static_cast(state.get().feeReserveBaseline), state.get().feeReserveGuardDropBasisPoints), - NOST_BASIS_POINTS_SCALE); - if (static_cast(locals.reserveDrop) >= locals.guardDropThreshold && - locals.guardElapsedSeconds <= state.get().feeReserveGuardWindowSeconds) - { - state.mut().isEmergencyPaused = 1; - state.mut().emergencyPausedAt = locals.currentDate; - state.mut().feeReserveBaselineAt.setInvalid(); - } - else if (locals.guardElapsedSeconds >= state.get().feeReserveGuardWindowSeconds) - { - state.mut().feeReserveBaseline = locals.currentReserve; - state.mut().feeReserveBaselineAt = locals.currentDate; - } - } - else if (locals.guardElapsedSeconds >= state.get().feeReserveGuardWindowSeconds) - { - state.mut().feeReserveBaseline = locals.currentReserve; - state.mut().feeReserveBaselineAt = locals.currentDate; - } - } - } - - CALL(SyncAuctionPauseState, locals.syncAuctionPauseStateInput, locals.syncAuctionPauseStateOutput); - // Lifecycle transitions must not advance while SyncAuctionPauseState still owns the global timer freeze. - if (state.get().isAuctionTimerPaused) - { - return; - } - - // Only live auctions advance after pause synchronization has extended their timers. - locals.auctionIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); - while (locals.auctionIndex != NULL_INDEX) - { - locals.auction = state.get().auctionList.value(locals.auctionIndex); - switch (locals.auction.core.status) - { - case EAuctionStatus::Active: - diffDateInSecond(locals.auction.core.createdAt, locals.currentDate, locals.elapsedSeconds); - // Only an elapsed active auction is eligible for automatic settlement or seller-decision transition. - if (locals.elapsedSeconds >= locals.auction.core.auctionDurationSeconds) - { - switch (locals.auction.core.type) - { - case EAuctionType::Batch: - locals.finalizeBatchAuctionInput.auctionIndex = locals.auction.core.auctionIndex; - locals.finalizeBatchAuctionInput.currentDate = locals.currentDate; - CALL(FinalizeBatchAuction, locals.finalizeBatchAuctionInput, locals.finalizeBatchAuctionOutput); - break; - case EAuctionType::Standard: - // No-bid and reserve-satisfying outcomes are deterministic and need no seller approval window. - if (locals.auction.core.highestBidAmount == 0 || locals.auction.core.highestBidPrice >= locals.auction.core.salePrice) - { - locals.finalizeStandardAuctionInput.auctionIndex = locals.auction.core.auctionIndex; - locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; - CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); - } - else - { - // A funded bid below the seller's sale price requires an explicit, time-bounded seller choice. - // Below-sale standard bids enter a seller decision window instead of settling immediately. - locals.auction.core.status = EAuctionStatus::PendingSellerDecision; - locals.auction.core.sellerDecisionDeadline = locals.currentDate; - locals.auction.core.sellerDecisionDeadline.add(0, 0, 0, 0, 0, NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS); - state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); - } - break; - default: break; - }; - } - break; - case EAuctionStatus::PendingSellerDecision: - switch (locals.auction.core.type) - { - case EAuctionType::Standard: - // Expiry resolves in favor of the recorded highest bidder so the seller cannot lock escrow indefinitely. - if (locals.auction.core.sellerDecisionDeadline <= locals.currentDate) - { - locals.finalizeStandardAuctionInput.auctionIndex = locals.auction.core.auctionIndex; - locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; - CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); - } - break; - default: break; - } - break; - default: break; - } - - locals.auctionIndex = state.get().auctionList.nextElementIndex(locals.auctionIndex); - } - } - - /** - * @brief Validates auction lot entries and totals the escrowed quantity. - */ - PRIVATE_FUNCTION_WITH_LOCALS(AnalyzeAuctionLot) - { - output.totalEscrowQuantity = 0; - output.lotItemCount = 0; - output.isValid = 0; - - // Lot validation also enforces the configured maximum auction lifetime. - if (input.durationDays == 0 || input.durationDays > state.get().maxAuctionDurationDays) - { - return; - } - - // Scan the full fixed ABI array because valid entries may be followed only by zero-padded slots. - for (locals.lotItemIndex = 0; locals.lotItemIndex < input.auctionLotItems.capacity(); ++locals.lotItemIndex) - { - locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); - // A zero asset is padding only when its paired quantity is also zero. - if (isZeroAsset(locals.lotItem.asset)) - { - if (locals.lotItem.quantity != 0) - { - return; - } - continue; - } - - if (locals.lotItem.quantity <= 0) - { - return; - } - - output.lotItemCount = sadd(output.lotItemCount, 1ULL); - output.totalEscrowQuantity = sadd(output.totalEscrowQuantity, static_cast(locals.lotItem.quantity)); - } - - output.isValid = output.lotItemCount > 0 ? 1 : 0; - } - - /** - * @brief Resolves whether the current tick belongs to a scheduled auction pause window. - */ - PRIVATE_FUNCTION_WITH_LOCALS(GetAuctionPauseState) - { - output.isPaused = 0; - output.pauseStartedAt.setInvalid(); - output.pauseEndsAt.setInvalid(); - - // The initial runtime date is treated as a full-day launch pause. - locals.currentDate = qpi.now(); - makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentDateStamp); - if (locals.currentDateStamp == NOST_DEFAULT_INIT_TIME) - { - output.isPaused = 1; - output.pauseStartedAt = locals.currentDate; - output.pauseStartedAt.setTime(0, 0, 0, 0, 0); - output.pauseEndsAt = output.pauseStartedAt; - output.pauseEndsAt.addDays(1); - return; - } - - // Scheduled pre-epoch pauses keep auctions from expiring during the transition window. - if (qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()) == NOST_PRE_EPOCH_PAUSE_DAY_OF_WEEK && qpi.hour() == NOST_PRE_EPOCH_PAUSE_HOUR && - qpi.minute() >= NOST_PRE_EPOCH_PAUSE_MINUTE) - { - output.isPaused = 1; - output.pauseStartedAt = locals.currentDate; - output.pauseStartedAt.setTime(NOST_PRE_EPOCH_PAUSE_HOUR, NOST_PRE_EPOCH_PAUSE_MINUTE, 0, 0, 0); - output.pauseEndsAt = output.pauseStartedAt; - output.pauseEndsAt.add(0, 0, 0, 0, 0, NOST_AUCTION_PRE_EPOCH_PAUSE_SECONDS); - } - } - - /** - * @brief Reports whether user-facing auction interactions are currently paused. - */ - PRIVATE_FUNCTION(IsAuctionInteractionPaused) - { - // Emergency pause takes precedence over scheduled and post-epoch launch pauses. - if (state.get().isEmergencyPaused) - { - output.isPaused = 1; - return; - } - - output.isPaused = state.get().isAuctionTimerPaused; - if (output.isPaused) - { - return; - } - - output.isPaused = state.get().isPostBeginEpochPauseArmed && (qpi.tick() - qpi.initialTick()) < NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS; - } - - /** - * @brief Synchronizes timer pause state and extends affected auction deadlines. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(SyncAuctionPauseState) - { - locals.currentDate = qpi.now(); - - // While emergency pause is active, keep extending the timer pause window. - if (state.get().isEmergencyPaused) - { - if (!state.get().isAuctionTimerPaused) - { - state.mut().isAuctionTimerPaused = 1; - state.mut().auctionTimerPauseStartedAt = locals.currentDate; - state.mut().auctionTimerPauseEndsAt = locals.currentDate; - } - else - { - state.mut().auctionTimerPauseEndsAt = locals.currentDate; - } - return; - } - - CALL(GetAuctionPauseState, locals.getAuctionPauseStateInput, locals.getAuctionPauseStateOutput); - - // The launch pause can overlap the scheduled pause; merge both windows before timers resume. - if (state.get().isPostBeginEpochPauseArmed) - { - if ((qpi.tick() - qpi.initialTick()) < NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) - { - if (!state.get().isAuctionTimerPaused) - { - state.mut().isAuctionTimerPaused = 1; - state.mut().auctionTimerPauseStartedAt = locals.currentDate; - state.mut().auctionTimerPauseEndsAt = locals.currentDate; - } - else - { - if (!state.get().auctionTimerPauseStartedAt.isValid()) - { - state.mut().auctionTimerPauseStartedAt = locals.currentDate; - } - if (!state.get().auctionTimerPauseEndsAt.isValid() || locals.currentDate > state.get().auctionTimerPauseEndsAt) - { - state.mut().auctionTimerPauseEndsAt = locals.currentDate; - } - } - - if (locals.getAuctionPauseStateOutput.isPaused) - { - if (!state.get().auctionTimerPauseStartedAt.isValid() || - locals.getAuctionPauseStateOutput.pauseStartedAt < state.get().auctionTimerPauseStartedAt) - { - state.mut().auctionTimerPauseStartedAt = locals.getAuctionPauseStateOutput.pauseStartedAt; - } - if (!state.get().auctionTimerPauseEndsAt.isValid() || - locals.getAuctionPauseStateOutput.pauseEndsAt > state.get().auctionTimerPauseEndsAt) - { - state.mut().auctionTimerPauseEndsAt = locals.getAuctionPauseStateOutput.pauseEndsAt; - } - } - return; - } - - state.mut().isPostBeginEpochPauseArmed = 0; - } - - // Scheduled pauses are recorded as a window that will later be added to all active deadlines. - if (locals.getAuctionPauseStateOutput.isPaused) - { - if (!state.get().isAuctionTimerPaused) - { - state.mut().isAuctionTimerPaused = 1; - state.mut().auctionTimerPauseStartedAt = locals.getAuctionPauseStateOutput.pauseStartedAt; - state.mut().auctionTimerPauseEndsAt = locals.getAuctionPauseStateOutput.pauseEndsAt; - return; - } - - if (!state.get().auctionTimerPauseStartedAt.isValid() || - locals.getAuctionPauseStateOutput.pauseStartedAt < state.get().auctionTimerPauseStartedAt) - { - state.mut().auctionTimerPauseStartedAt = locals.getAuctionPauseStateOutput.pauseStartedAt; - } - if (!state.get().auctionTimerPauseEndsAt.isValid() || locals.getAuctionPauseStateOutput.pauseEndsAt > state.get().auctionTimerPauseEndsAt) - { - state.mut().auctionTimerPauseEndsAt = locals.getAuctionPauseStateOutput.pauseEndsAt; - } - return; - } - - if (!state.get().isAuctionTimerPaused) - { - return; - } - - if (!state.get().auctionTimerPauseStartedAt.isValid() || !state.get().auctionTimerPauseEndsAt.isValid()) - { - state.mut().isAuctionTimerPaused = 0; - state.mut().auctionTimerPauseStartedAt.setInvalid(); - state.mut().auctionTimerPauseEndsAt.setInvalid(); - return; - } - - // When the pause ends, preserve elapsed auction time by extending every affected deadline. - diffDateInSecond(state.get().auctionTimerPauseStartedAt, state.get().auctionTimerPauseEndsAt, locals.pausedSeconds); - if (locals.pausedSeconds > 0) - { - locals.auctionIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); - while (locals.auctionIndex != NULL_INDEX) - { - locals.auction = state.get().auctionList.value(locals.auctionIndex); - if (locals.auction.core.status == EAuctionStatus::Active) - { - locals.auction.core.auctionDurationSeconds = sadd(locals.auction.core.auctionDurationSeconds, locals.pausedSeconds); - state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); - } - else if (locals.auction.core.status == EAuctionStatus::PendingSellerDecision && locals.auction.core.sellerDecisionDeadline.isValid()) - { - locals.auction.core.sellerDecisionDeadline.add(0, 0, 0, 0, 0, static_cast(locals.pausedSeconds)); - state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); - } - locals.auctionIndex = state.get().auctionList.nextElementIndex(locals.auctionIndex); - } - } - - state.mut().isAuctionTimerPaused = 0; - state.mut().auctionTimerPauseStartedAt.setInvalid(); - state.mut().auctionTimerPauseEndsAt.setInvalid(); - } - - /** - * @brief Returns remaining launch-delay ticks after the current epoch begins. - */ - PRIVATE_FUNCTION_WITH_LOCALS(GetTicksBeforeAuctionLaunchInternal) - { - output.ticks = 0; - - // An unarmed delay has no remaining ticks even if the current tick is near the epoch boundary. - if (!state.get().isPostBeginEpochPauseArmed) - { - return; - } - - output.ticks = static_cast(max(static_cast(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) - - (static_cast(qpi.tick()) - static_cast(qpi.initialTick())), - 0)); - } - - /** - * @brief Registers a QU obligation before the associated settlement becomes final. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(QueueQuPayout) - { - output.success = 0; - // Zero is an idempotent no-op, while a non-zero obligation must always have a payable recipient. - if (input.amount == 0 || isZero(input.recipient)) - { - output.success = input.amount == 0; - return; - } - - locals.previousAmount = 0; - // Reject aggregate overflow before touching either the per-wallet entry or its mirrored total. - if (input.amount > UINT64_MAX - state.get().totalPendingQuPayouts) - { - return; - } - - // Multiple settlements for one wallet share one liability entry to conserve bounded map capacity. - if (state.get().pendingQuPayouts.get(input.recipient, locals.previousAmount)) - { - // The wallet-level value must remain exactly reconcilable with totalPendingQuPayouts. - if (input.amount > UINT64_MAX - locals.previousAmount) - { - return; - } - - locals.updatedAmount = sadd(locals.previousAmount, input.amount); - if (!state.mut().pendingQuPayouts.replace(input.recipient, locals.updatedAmount)) - { - return; - } - } - else - { - // First-time recipients consume a new map slot; failure leaves the global liability total unchanged. - locals.payoutIndex = state.mut().pendingQuPayouts.set(input.recipient, input.amount); - if (locals.payoutIndex == NULL_INDEX) - { - return; - } - } - - state.mut().totalPendingQuPayouts = sadd(state.get().totalPendingQuPayouts, input.amount); - output.success = 1; - } - - /** - * @brief Pays a bounded number of chunks and preserves every unpaid remainder in state. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(FlushQuPayout) - { - output.success = 0; - output.transferredAmount = 0; - output.remainingAmount = 0; - // Absence is distinct from a paid zero balance because zero-balance entries are removed immediately. - if (!state.get().pendingQuPayouts.get(input.recipient, output.remainingAmount)) - { - return; - } - - locals.chunkIndex = 0; - // Bound both transfer size and iteration count so one payout attempt cannot exhaust contract execution time. - while (output.remainingAmount > 0 && locals.chunkIndex < input.maxChunks) - { - locals.chunkAmount = min(output.remainingAmount, static_cast(MAX_AMOUNT)); - locals.transferResult = qpi.transfer(input.recipient, static_cast(locals.chunkAmount)); - // A failed transfer stops delivery without decrementing the durable obligation. - if (locals.transferResult < 0) - { - break; - } - output.remainingAmount -= locals.chunkAmount; - output.transferredAmount = sadd(output.transferredAmount, locals.chunkAmount); - state.mut().totalPendingQuPayouts -= locals.chunkAmount; - ++locals.chunkIndex; - } - - // Fully discharged entries release map capacity; partial delivery persists the exact remainder for retry. - if (output.remainingAmount == 0) - { - state.mut().pendingQuPayouts.removeByKey(input.recipient); - } - else - { - state.mut().pendingQuPayouts.replace(input.recipient, output.remainingAmount); - } - output.success = 1; - } - - /** - * @brief Retries a bounded set of pending QU payouts and advances the persistent round-robin cursor. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(ProcessPendingQuPayouts) - { - locals.payoutScanIndex = mod(state.get().pendingPayoutScanCursor, state.get().pendingQuPayouts.capacity()); - locals.payoutTargetRecipientCount = min(state.get().pendingQuPayouts.population(), NOST_END_EPOCH_PAYOUT_RECIPIENT_NUM); - locals.processedPayoutRecipientCount = 0; - locals.payoutElementIndex = state.get().pendingQuPayouts.nextElementIndex(static_cast(locals.payoutScanIndex) - 1); - // Round-robin scanning bounds epoch work and prevents a permanently failing wallet from starving later map slots. - while (locals.processedPayoutRecipientCount < locals.payoutTargetRecipientCount) - { - // Wrap once the physical end is reached; the initial population snapshot prevents duplicate processing. - if (locals.payoutElementIndex == NULL_INDEX) - { - locals.payoutElementIndex = state.get().pendingQuPayouts.nextElementIndex(NULL_INDEX); - if (locals.payoutElementIndex == NULL_INDEX) - { - break; - } - } - locals.pendingPayoutRecipient = state.get().pendingQuPayouts.key(locals.payoutElementIndex); - locals.payoutScanIndex = mod(sadd(static_cast(locals.payoutElementIndex), 1ULL), state.get().pendingQuPayouts.capacity()); - locals.flushQuPayoutInput.recipient = locals.pendingPayoutRecipient; - locals.flushQuPayoutInput.maxChunks = NOST_END_EPOCH_PAYOUT_CHUNKS_PER_RECIPIENT; - CALL(FlushQuPayout, locals.flushQuPayoutInput, locals.flushQuPayoutOutput); - locals.processedPayoutRecipientCount = sadd(locals.processedPayoutRecipientCount, 1ULL); - locals.payoutElementIndex = state.get().pendingQuPayouts.nextElementIndex(locals.payoutElementIndex); - } - state.mut().pendingPayoutScanCursor = locals.payoutScanIndex; - } - - /** - * @brief Registers a payout exactly once for this call and immediately attempts bounded delivery. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(QueueAndFlushQuPayout) - { - output.success = 0; - output.transferredAmount = 0; - output.remainingAmount = 0; - locals.queueQuPayoutInput.recipient = input.recipient; - locals.queueQuPayoutInput.amount = input.amount; - CALL(QueueQuPayout, locals.queueQuPayoutInput, locals.queueQuPayoutOutput); - // Never attempt delivery unless the complete liability was made durable first. - if (!locals.queueQuPayoutOutput.success) - { - return; - } - // QueueQuPayout treats zero as success, but there is no map entry for FlushQuPayout to consume. - if (input.amount == 0) - { - output.success = 1; - return; - } - locals.flushQuPayoutInput.recipient = input.recipient; - locals.flushQuPayoutInput.maxChunks = input.maxChunks; - CALL(FlushQuPayout, locals.flushQuPayoutInput, locals.flushQuPayoutOutput); - output.transferredAmount = locals.flushQuPayoutOutput.transferredAmount; - output.remainingAmount = locals.flushQuPayoutOutput.remainingAmount; - output.success = locals.flushQuPayoutOutput.success; - } - - /** - * @brief Appends a participant snapshot to bounded history. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(ArchiveParticipant) - { - locals.historyIndex = mod(state.get().participantHistoryCounter, state.get().participantHistory.capacity()); - state.mut().participantHistory.set(locals.historyIndex, input.participantData); - state.mut().participantHistoryCounter = sadd(state.get().participantHistoryCounter, 1ULL); - } - - /** - * @brief Archives a closed auction and releases its active hash-map slot. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(ArchiveClosedAuction) - { - locals.historyIndex = mod(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); - state.mut().closedAuctionHistory.set(locals.historyIndex, input.auction); - state.mut().closedAuctionHistoryCounter = sadd(state.get().closedAuctionHistoryCounter, 1ULL); - state.mut().auctionList.removeByKey(input.auction.core.auctionIndex); - } - - /** - * @brief Finds a live auction or a retained closed-auction snapshot. - */ - PRIVATE_FUNCTION_WITH_LOCALS(FindAuction) - { - output.found = state.get().auctionList.get(input.auctionIndex, output.auction); - // Active storage is authoritative and avoids the bounded linear archive scan for live auctions. - if (output.found) - { - return; - } - // Closed auctions remain queryable only while their full snapshot is retained in the ring buffer. - for (locals.historyIndex = 0; locals.historyIndex < state.get().closedAuctionHistory.capacity(); ++locals.historyIndex) - { - locals.archivedAuction = state.get().closedAuctionHistory.get(locals.historyIndex); - if (locals.archivedAuction.core.status != EAuctionStatus::None && locals.archivedAuction.core.auctionIndex == input.auctionIndex) - { - output.auction = locals.archivedAuction; - output.found = 1; - return; - } - } - } - - /** - * @brief Tests retained closed history without copying an auction into the caller's locals. - */ - PRIVATE_FUNCTION_WITH_LOCALS(IsClosedAuctionRetained) - { - output.found = 0; - locals.retainedClosedAuctionCount = min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); - // Only initialized ring-buffer entries can match; inspect the const snapshot in place to keep this lookup lightweight. - for (locals.historyIndex = 0; locals.historyIndex < locals.retainedClosedAuctionCount; ++locals.historyIndex) - { - if (state.get().closedAuctionHistory.get(locals.historyIndex).core.status != EAuctionStatus::None && - state.get().closedAuctionHistory.get(locals.historyIndex).core.auctionIndex == input.auctionIndex) - { - output.found = 1; - return; - } - } - } - - /** - * @brief Selects the smallest retained auction index after an optional cursor. - */ - PRIVATE_FUNCTION_WITH_LOCALS(SelectNextRetainedAuction) - { - output.found = 0; - // Hash-map iteration is not creation ordered, so retain the smallest eligible live index beyond the cursor. - for (locals.auctionElementIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); locals.auctionElementIndex != NULL_INDEX; - locals.auctionElementIndex = state.get().auctionList.nextElementIndex(locals.auctionElementIndex)) - { - locals.candidateAuction = state.get().auctionList.value(locals.auctionElementIndex); - // Apply the cursor and optional seller filter before comparing creation indices. - if ((input.hasAfterAuctionIndex && locals.candidateAuction.core.auctionIndex <= input.afterAuctionIndex) || - (input.filterBySeller && locals.candidateAuction.core.seller != input.seller)) - { - continue; - } - if (!output.found || locals.candidateAuction.core.auctionIndex < output.auction.core.auctionIndex) - { - output.auction = locals.candidateAuction; - output.found = 1; - } - } - // Live-only callers avoid the archive scan entirely. - if (!input.includeClosedAuctions) - { - return; - } - - locals.retainedClosedAuctionCount = min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); - // Merge only retained closed snapshots without assuming physical ring order. - for (locals.historyIndex = 0; locals.historyIndex < locals.retainedClosedAuctionCount; ++locals.historyIndex) - { - locals.candidateAuction = state.get().closedAuctionHistory.get(locals.historyIndex); - // Apply the same cursor and seller filter to archived candidates. - if (locals.candidateAuction.core.status == EAuctionStatus::None || - (input.hasAfterAuctionIndex && locals.candidateAuction.core.auctionIndex <= input.afterAuctionIndex) || - (input.filterBySeller && locals.candidateAuction.core.seller != input.seller)) - { - continue; - } - if (!output.found || locals.candidateAuction.core.auctionIndex < output.auction.core.auctionIndex) - { - output.auction = locals.candidateAuction; - output.found = 1; - } - } - } - - /** - * @brief Counts retained live and closed auctions belonging to one seller without reconstructing creation order. - */ - PRIVATE_FUNCTION_WITH_LOCALS(CountRetainedAuctionsBySeller) - { - output.count = 0; - // A physical live-map pass is sufficient because counting does not depend on creation order. - for (locals.auctionElementIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); locals.auctionElementIndex != NULL_INDEX; - locals.auctionElementIndex = state.get().auctionList.nextElementIndex(locals.auctionElementIndex)) - { - locals.candidateAuction = state.get().auctionList.value(locals.auctionElementIndex); - if (locals.candidateAuction.core.seller == input.seller) - { - output.count = sadd(output.count, 1ULL); - } - } - - locals.retainedClosedAuctionCount = min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); - // Only initialized ring slots can contribute to the retained seller count. - for (locals.historyIndex = 0; locals.historyIndex < locals.retainedClosedAuctionCount; ++locals.historyIndex) - { - locals.candidateAuction = state.get().closedAuctionHistory.get(locals.historyIndex); - if (locals.candidateAuction.core.status != EAuctionStatus::None && locals.candidateAuction.core.seller == input.seller) - { - output.count = sadd(output.count, 1ULL); - } - } - } - - /** - * @brief Finds the smallest retained auction index whose complete fixed-size metadata CID matches the input. - */ - PRIVATE_FUNCTION_WITH_LOCALS(FindFirstRetainedAuctionByMetadataCid) - { - output.found = 0; - // Select the minimum matching live index directly instead of repeatedly reconstructing global order. - for (locals.auctionElementIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); locals.auctionElementIndex != NULL_INDEX; - locals.auctionElementIndex = state.get().auctionList.nextElementIndex(locals.auctionElementIndex)) - { - locals.candidateAuction = state.get().auctionList.value(locals.auctionElementIndex); - locals.metadataMatches = 1; - // Compare the complete fixed CID field, including zero padding. - for (locals.metadataIndex = 0; locals.metadataIndex < NOST_AUCTION_METADATA_CID_LENGTH; ++locals.metadataIndex) - { - if (locals.candidateAuction.core.metadataIpfsCid.get(locals.metadataIndex) != input.metadataIpfsCid.get(locals.metadataIndex)) - { - locals.metadataMatches = 0; - break; - } - } - if (locals.metadataMatches && (!output.found || locals.candidateAuction.core.auctionIndex < output.auction.core.auctionIndex)) - { - output.auction = locals.candidateAuction; - output.found = 1; - } - } - - locals.retainedClosedAuctionCount = min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); - // Closed snapshots share the same index ordering but occupy unordered ring slots. - for (locals.historyIndex = 0; locals.historyIndex < locals.retainedClosedAuctionCount; ++locals.historyIndex) - { - locals.candidateAuction = state.get().closedAuctionHistory.get(locals.historyIndex); - if (locals.candidateAuction.core.status == EAuctionStatus::None) - { - continue; - } - locals.metadataMatches = 1; - // Compare the complete fixed CID field, including zero padding. - for (locals.metadataIndex = 0; locals.metadataIndex < NOST_AUCTION_METADATA_CID_LENGTH; ++locals.metadataIndex) - { - if (locals.candidateAuction.core.metadataIpfsCid.get(locals.metadataIndex) != input.metadataIpfsCid.get(locals.metadataIndex)) - { - locals.metadataMatches = 0; - break; - } - } - if (locals.metadataMatches && (!output.found || locals.candidateAuction.core.auctionIndex < output.auction.core.auctionIndex)) - { - output.auction = locals.candidateAuction; - output.found = 1; - } - } - } - - /** - * @brief Pays auction sale proceeds to the seller and records every fee for end-of-epoch settlement. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(DistributeAuctionRevenue) - { - output.sellerPayout = input.grossAmount; - output.success = 0; - - // Zero-gross settlements still report success so callers can close no-sale auctions cleanly. - if (input.grossAmount == 0) - { - output.success = 1; - return; - } - // Only the seller is queued during settlement; fee recipients are handled by END_EPOCH. - if (state.get().pendingQuPayouts.population() > state.get().pendingQuPayouts.capacity() - NOST_AUCTION_REVENUE_MAX_PAYOUT_RECIPIENTS) - { - return; - } - - calculateAuctionRevenueBreakdown(input.grossAmount, state, locals.auctionRevenueBreakdown); - output.sellerPayout = locals.auctionRevenueBreakdown.sellerPayout; - - // Register the seller liability before recording fees so a queue-capacity failure cannot duplicate fee accrual on retry. - locals.payoutInput.recipient = input.seller; - locals.payoutInput.amount = output.sellerPayout; - locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; - CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); - if (!locals.payoutOutput.success) - { - return; - } - - locals.feePool = state.get().feePool; - // The routing decision and fee configuration are captured when revenue is settled; recipient wallets are resolved at END_EPOCH. - if (routeAllFeesToDevelopment(state)) - { - locals.feePool.developmentAmount = sadd(locals.feePool.developmentAmount, input.grossAmount - output.sellerPayout); - } - else - { - locals.shareholderFeeTierIndex = getAuctionShareholderFeeTierIndex(input.grossAmount); - switch (locals.shareholderFeeTierIndex) - { - case 0: - locals.feePool.shareholderDividendTier1Amount = - sadd(locals.feePool.shareholderDividendTier1Amount, locals.auctionRevenueBreakdown.shareholderDividendAmount); - break; - case 1: - locals.feePool.shareholderDividendTier2Amount = - sadd(locals.feePool.shareholderDividendTier2Amount, locals.auctionRevenueBreakdown.shareholderDividendAmount); - break; - case 2: - locals.feePool.shareholderDividendTier3Amount = - sadd(locals.feePool.shareholderDividendTier3Amount, locals.auctionRevenueBreakdown.shareholderDividendAmount); - break; - default: - locals.feePool.shareholderDividendTier4Amount = - sadd(locals.feePool.shareholderDividendTier4Amount, locals.auctionRevenueBreakdown.shareholderDividendAmount); - break; - } - - locals.feePool.managementAmount = sadd(locals.feePool.managementAmount, locals.auctionRevenueBreakdown.managementFeeAmount); - locals.feePool.developmentAmount = sadd(locals.feePool.developmentAmount, locals.auctionRevenueBreakdown.developmentFeeAmount); - locals.feePool.takeoverCoordinatorAmount = - sadd(locals.feePool.takeoverCoordinatorAmount, locals.auctionRevenueBreakdown.takeoverCoordinatorFeeAmount); - } - - state.mut().feePool = locals.feePool; - output.success = 1; - } - - /** - * @brief Accumulates a service fee using the routing mode active when the fee is charged. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(AccumulateAuctionServiceFee) - { - output.success = 0; - - // Creation, bidding, and cancellation paths may call this with zero after fee configuration changes. - if (input.feeAmount == 0) - { - output.success = 1; - return; - } - - locals.feePool = state.get().feePool; - if (routeAllFeesToDevelopment(state)) - { - locals.feePool.developmentAmount = sadd(locals.feePool.developmentAmount, input.feeAmount); - } - else - { - locals.feePool.commonServiceFeeAmount = sadd(locals.feePool.commonServiceFeeAmount, input.feeAmount); - } - state.mut().feePool = locals.feePool; - output.success = 1; - } - - /** - * @brief Materializes compatible service fees and settles every shared pool accumulator using the recipients active at `END_EPOCH`. - * @note Each accumulator is cleared only after its value has moved to dividend dust or a durable payout liability. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(DistributeNostromoFeePool) - { - output.success = 0; - locals.feePool = state.get().feePool; - - if (locals.feePool.commonServiceFeeAmount > 0) - { - calculateAuctionServiceFeeBreakdown(locals.feePool.commonServiceFeeAmount, locals.auctionServiceFeeBreakdown); - locals.feePool.shareholderDividendAmount = - sadd(locals.feePool.shareholderDividendAmount, locals.auctionServiceFeeBreakdown.shareholderDividendAmount); - locals.feePool.managementAmount = sadd(locals.feePool.managementAmount, locals.auctionServiceFeeBreakdown.managementFeeAmount); - locals.feePool.developmentAmount = sadd(locals.feePool.developmentAmount, locals.auctionServiceFeeBreakdown.developmentFeeAmount); - locals.feePool.takeoverCoordinatorAmount = - sadd(locals.feePool.takeoverCoordinatorAmount, locals.auctionServiceFeeBreakdown.takeoverCoordinatorFeeAmount); - locals.feePool.commonServiceFeeAmount = 0; - state.mut().feePool = locals.feePool; - } - - locals.shareholderDividendAmount = - sadd(sadd(sadd(locals.feePool.shareholderDividendTier1Amount, locals.feePool.shareholderDividendTier2Amount), - sadd(locals.feePool.shareholderDividendTier3Amount, locals.feePool.shareholderDividendTier4Amount)), - locals.feePool.shareholderDividendAmount); - if (locals.shareholderDividendAmount > 0) - { - state.mut().auctionShareholderDividendPool = sadd(state.get().auctionShareholderDividendPool, locals.shareholderDividendAmount); - locals.feePool.shareholderDividendTier1Amount = 0; - locals.feePool.shareholderDividendTier2Amount = 0; - locals.feePool.shareholderDividendTier3Amount = 0; - locals.feePool.shareholderDividendTier4Amount = 0; - locals.feePool.shareholderDividendAmount = 0; - state.mut().feePool = locals.feePool; - } - - locals.dividendPerShare = div(state.get().auctionShareholderDividendPool, NUMBER_OF_COMPUTORS); - if (locals.dividendPerShare > 0 && qpi.distributeDividends(locals.dividendPerShare)) - { - locals.distributedDividendAmount = smul(locals.dividendPerShare, static_cast(NUMBER_OF_COMPUTORS)); - state.mut().auctionShareholderDividendPool -= locals.distributedDividendAmount; - } - - if (state.get().feePool.managementAmount > 0) - { - locals.payoutInput.recipient = state.get().management; - locals.payoutInput.amount = state.get().feePool.managementAmount; - locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; - CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); - if (!locals.payoutOutput.success) - { - return; - } - state.mut().feePool.managementAmount = 0; - } - if (state.get().feePool.developmentAmount > 0) - { - locals.payoutInput.recipient = state.get().development; - locals.payoutInput.amount = state.get().feePool.developmentAmount; - locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; - CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); - if (!locals.payoutOutput.success) - { - return; - } - state.mut().feePool.developmentAmount = 0; - } - if (state.get().feePool.takeoverCoordinatorAmount > 0) - { - locals.payoutInput.recipient = state.get().takeoverCoordinator; - locals.payoutInput.amount = state.get().feePool.takeoverCoordinatorAmount; - locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; - CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); - if (!locals.payoutOutput.success) - { - return; - } - state.mut().feePool.takeoverCoordinatorAmount = 0; - } - - output.success = 1; - } - - /** - * @brief Counts non-empty wallet entries allowed to bid in a private auction. - */ - PRIVATE_FUNCTION_WITH_LOCALS(CountAllowedBidderWallets) - { - output.allowedWalletCount = 0; - for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < input.allowedBidderWallets.capacity(); ++locals.allowedWalletIndex) - { - if (!isZero(input.allowedBidderWallets.get(locals.allowedWalletIndex))) - { - output.allowedWalletCount = sadd(output.allowedWalletCount, 1ULL); - } - } - } - - /** - * @brief Counts valid access-asset requirements for private auction gating. - */ - PRIVATE_FUNCTION_WITH_LOCALS(CountRequiredAccessAssets) - { - output.requiredAccessAssetCount = 0; - output.isValid = 1; - // Empty asset slots are allowed only when their quantity is also empty. - for (locals.requiredAccessAssetIndex = 0; locals.requiredAccessAssetIndex < input.requiredAccessAssets.capacity(); - ++locals.requiredAccessAssetIndex) - { - locals.requiredAccessAsset = input.requiredAccessAssets.get(locals.requiredAccessAssetIndex); - if (isZeroAsset(locals.requiredAccessAsset.asset)) - { - if (locals.requiredAccessAsset.quantity != 0) - { - output.isValid = 0; - return; - } - continue; - } - - if (locals.requiredAccessAsset.quantity <= 0) - { - output.isValid = 0; - return; - } - - output.requiredAccessAssetCount = sadd(output.requiredAccessAssetCount, 1ULL); - } - } - - /** - * @brief Checks whether the invocator owns at least one configured access asset. - */ - PRIVATE_FUNCTION_WITH_LOCALS(HasRequiredAccessAsset) - { - output.hasRequiredAccessAsset = 0; - if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) - { - return; - } - - // Owning any one configured access asset at the required quantity grants private auction access. - for (locals.requiredAccessAssetSetIndex = locals.auction.requiredAccessAssets.nextElementIndex(NULL_INDEX); - locals.requiredAccessAssetSetIndex != NULL_INDEX; - locals.requiredAccessAssetSetIndex = locals.auction.requiredAccessAssets.nextElementIndex(locals.requiredAccessAssetSetIndex)) - { - locals.requiredAccessAsset.asset = locals.auction.requiredAccessAssets.key(locals.requiredAccessAssetSetIndex); - locals.requiredAccessAsset.quantity = locals.auction.requiredAccessAssets.value(locals.requiredAccessAssetSetIndex); - locals.possessedAccessShares = qpi.numberOfShares(locals.requiredAccessAsset.asset, AssetOwnershipSelect::byOwner(qpi.invocator()), - AssetPossessionSelect::byPossessor(qpi.invocator())); - if (locals.possessedAccessShares >= locals.requiredAccessAsset.quantity) - { - output.hasRequiredAccessAsset = 1; - return; - } - } - } - - /** - * @brief Recomputes the displayed highest active Batch Auction bid. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(RecomputeBatchHighestBid) - { - locals.bestParticipantFound = 0; - - if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) - { - return; - } - - if (locals.auction.core.type != EAuctionType::Batch) - { - return; - } - - // Batch auctions expose the highest active price, with FIFO tie-breaking for equal bids. - for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) - { - locals.participantData = state.get().participants.get(locals.participantIndex); - if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) - { - continue; - } - - if (!locals.participantData.isActive || locals.participantData.escrowedAmount == 0) - { - continue; - } - - if (!locals.bestParticipantFound || locals.participantData.bidAmount > locals.bestParticipantData.bidAmount || - (locals.participantData.bidAmount == locals.bestParticipantData.bidAmount && - locals.participantData.bidIndex < locals.bestParticipantData.bidIndex)) - { - locals.bestParticipantFound = 1; - locals.bestParticipantData = locals.participantData; - locals.bestParticipantSlotIndex = locals.participantIndex; - } - } - - if (locals.bestParticipantFound) - { - locals.auction.core.highestBidder = locals.bestParticipantData.participant; - locals.auction.core.highestBidPrice = locals.bestParticipantData.bidAmount; - locals.auction.core.highestBidQuantity = locals.bestParticipantData.requestedQuantity; - locals.auction.core.highestBidAmount = locals.bestParticipantData.escrowedAmount; - locals.auction.core.highestBidSlotIndex = locals.bestParticipantSlotIndex; - } - else - { - locals.auction.core.highestBidAmount = 0; - locals.auction.core.highestBidPrice = 0; - locals.auction.core.highestBidQuantity = 0; - locals.auction.core.highestBidder = NULL_ID; - locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; - } - - state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); - } - - /** - * @brief Computes the price and quantity still available for a Batch Auction bid. - */ - PRIVATE_FUNCTION_WITH_LOCALS(ComputeBatchBidAvailability) - { - output.found = 0; - output.isAcceptingBids = 0; - output.minimumBidPrice = 0; - output.availableQuantity = 0; - locals.lowestWinningPriceFound = 0; - locals.lowestWinningPrice = 0; - locals.salePriorityQuantity = 0; - locals.priorityQuantity = 0; - - // Availability is defined only for a retained live auction; closed snapshots never accept bids. - if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) - { - return; - } - - output.found = 1; - if (locals.auction.core.type != EAuctionType::Batch || locals.auction.core.status != EAuctionStatus::Active || - locals.auction.core.quantityForSale < locals.auction.core.minimumPurchaseQuantity) - { - return; - } - - // Existing sale-price-or-better bids reserve priority quantity before a new bid can enter. - for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) - { - locals.participantData = state.get().participants.get(locals.participantIndex); - if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) - { - continue; - } - - if (!locals.participantData.isActive || locals.participantData.escrowedAmount == 0 || locals.participantData.requestedQuantity == 0) - { - continue; - } - - if (!locals.lowestWinningPriceFound || locals.participantData.bidAmount < locals.lowestWinningPrice) - { - locals.lowestWinningPriceFound = 1; - locals.lowestWinningPrice = locals.participantData.bidAmount; - } - - if (locals.participantData.bidAmount >= locals.auction.core.salePrice) - { - locals.salePriorityQuantity = sadd(locals.salePriorityQuantity, locals.participantData.requestedQuantity); - } - } - - locals.effectiveCoverageQuantity = - locals.auction.core.quantityForSale - locals.auction.core.minimumPurchaseQuantity + NOST_BATCH_COVERAGE_THRESHOLD_OFFSET; - // Once less than one minimum allocation remains, report no sale-price capacity instead of an unusable fragment. - if (locals.salePriorityQuantity >= locals.effectiveCoverageQuantity) - { - output.availableQuantity = 0; - } - else - { - // Otherwise expose the full unreserved quantity; the minimum check below decides whether bidding remains viable. - output.availableQuantity = locals.auction.core.quantityForSale - locals.salePriorityQuantity; - } - - // If sale-price capacity is exhausted, new bids must improve the current lowest winning price. - if (output.availableQuantity >= locals.auction.core.minimumPurchaseQuantity) - { - output.minimumBidPrice = locals.auction.core.salePrice; - output.isAcceptingBids = 1; - } - else - { - // A full book can still accept a strictly better bid that displaces the current lowest-priced allocation. - output.availableQuantity = 0; - if (!locals.lowestWinningPriceFound || locals.lowestWinningPrice == UINT64_MAX) - { - return; - } - - output.minimumBidPrice = sadd(locals.lowestWinningPrice, 1ULL); - output.isAcceptingBids = 1; - if (input.bidAmount == 0) - { - return; - } - } - - locals.outputPrice = input.bidAmount > 0 ? input.bidAmount : output.minimumBidPrice; - if (locals.outputPrice < output.minimumBidPrice) - { - output.availableQuantity = 0; - return; - } - - // Recompute capacity at the requested price so callers know the maximum acceptable quantity. - locals.priorityQuantity = 0; - for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) - { - locals.participantData = state.get().participants.get(locals.participantIndex); - if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) - { - continue; - } - - if (!locals.participantData.isActive || locals.participantData.escrowedAmount == 0 || locals.participantData.requestedQuantity == 0) - { - continue; - } - - if (locals.participantData.bidAmount > locals.outputPrice || locals.participantData.bidAmount == locals.outputPrice) - { - locals.priorityQuantity = sadd(locals.priorityQuantity, locals.participantData.requestedQuantity); - } - } - - // Equal-priced existing bids have FIFO priority, so a candidate at that price receives only later capacity. - if (locals.priorityQuantity >= locals.auction.core.quantityForSale) - { - output.availableQuantity = 0; - return; - } - - output.availableQuantity = locals.auction.core.quantityForSale - locals.priorityQuantity; - } - - /** - * @brief Validates, escrows, and ranks a new Batch Auction bid. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(ProcessBatchBid) - { - output.escrowedAmount = 0; - output.refundedAmount = 0; - output.errorCode = EAuctionError::Success; - output.success = 0; - if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) - { - output.refundedAmount = static_cast(qpi.invocationReward()); - output.errorCode = EAuctionError::AuctionNotFound; - return; - } - - if (input.effectiveQuantity < locals.auction.core.minimumPurchaseQuantity || input.bidAmount == 0) - { - output.refundedAmount = static_cast(qpi.invocationReward()); - output.errorCode = EAuctionError::InvalidInput; - return; - } - // Retain enough payout slots to refund every active participant plus the caller's possible overpayment. - if (state.get().pendingQuPayouts.population() > - state.get().pendingQuPayouts.capacity() - state.get().participants.capacity() - NOST_BATCH_BID_CALLER_PAYOUT_RECIPIENTS) - { - output.refundedAmount = static_cast(qpi.invocationReward()); - output.errorCode = EAuctionError::PayoutQueueFull; - return; - } - - calculateBatchAuctionBidFee(input.effectiveQuantity, input.bidAmount, locals.bidFeeCalculation); - if (locals.bidFeeCalculation.escrowAmount == 0) - { - output.refundedAmount = static_cast(qpi.invocationReward()); - output.errorCode = EAuctionError::InvalidInput; - return; - } - - if (input.bidAmount < locals.auction.core.salePrice) - { - output.refundedAmount = static_cast(qpi.invocationReward()); - output.errorCode = EAuctionError::BidTooLow; - return; - } - - locals.computeBatchBidAvailabilityInput.auctionIndex = input.auctionIndex; - locals.computeBatchBidAvailabilityInput.bidAmount = input.bidAmount; - CALL(ComputeBatchBidAvailability, locals.computeBatchBidAvailabilityInput, locals.computeBatchBidAvailabilityOutput); - if (!locals.computeBatchBidAvailabilityOutput.isAcceptingBids || input.bidAmount < locals.computeBatchBidAvailabilityOutput.minimumBidPrice) - { - output.refundedAmount = static_cast(qpi.invocationReward()); - output.errorCode = EAuctionError::BidTooLow; - return; - } - if (input.effectiveQuantity > locals.computeBatchBidAvailabilityOutput.availableQuantity) - { - output.refundedAmount = static_cast(qpi.invocationReward()); - output.errorCode = EAuctionError::QuantityUnavailable; - return; - } - - if (static_cast(qpi.invocationReward()) < locals.bidFeeCalculation.requiredReward) - { - output.refundedAmount = static_cast(qpi.invocationReward()); - output.errorCode = EAuctionError::InsufficientFunds; - return; - } - - // Batch bids consume live slots only; displaced and settled records move to the history ring. - locals.freeParticipantSlotFound = 0; - for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) - { - locals.participantData = state.get().participants.get(locals.participantIndex); - if (!locals.participantData.isUsed) - { - locals.freeParticipantSlotFound = 1; - locals.freeParticipantSlotIndex = locals.participantIndex; - break; - } - } - - if (!locals.freeParticipantSlotFound || locals.auction.core.nextBidIndex == UINT64_MAX) - { - output.refundedAmount = static_cast(qpi.invocationReward()); - output.errorCode = EAuctionError::StorageFull; - return; - } - - locals.participantData.escrowedAmount = locals.bidFeeCalculation.escrowAmount; - locals.participantData.requestedQuantity = input.effectiveQuantity; - locals.participantData.allocatedQuantity = 0; - locals.participantData.bidAmount = input.bidAmount; - locals.participantData.lastBidTime = input.currentDate; - locals.participantData.participant = qpi.invocator(); - locals.participantData.auctionIndex = input.auctionIndex; - locals.participantData.bidIndex = locals.auction.core.nextBidIndex; - locals.participantData.isUsed = 1; - locals.participantData.isActive = 1; - locals.participantData.isWinningBid = 1; - - // Accepted bids near deadline extend the auction to reduce last-moment sniping. - locals.auction.core.lastBidAt = input.currentDate; - if ((locals.auction.core.auctionDurationSeconds - input.elapsedSeconds) <= NOST_AUCTION_EXTENSION_SECONDS) - { - locals.auction.core.auctionDurationSeconds = sadd(locals.auction.core.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); - } - - locals.auction.core.nextBidIndex = sadd(locals.auction.core.nextBidIndex, 1ULL); - state.mut().participants.set(locals.freeParticipantSlotIndex, locals.participantData); - state.mut().auctionList.replace(input.auctionIndex, locals.auction); - - // Keep only the highest-priority quantity active; displaced escrow is refunded immediately. - locals.activeQuantity = 0; - for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) - { - locals.participantData = state.get().participants.get(locals.participantIndex); - if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) - { - continue; - } - if (locals.participantData.isActive && locals.participantData.escrowedAmount > 0 && locals.participantData.requestedQuantity > 0) - { - locals.activeQuantity = sadd(locals.activeQuantity, locals.participantData.requestedQuantity); - } - } - - // Repeatedly evict the lowest-priority tail until active demand fits the finite lot supply. - while (locals.activeQuantity > locals.auction.core.quantityForSale) - { - locals.worstParticipantFound = 0; - // Lowest price loses first; for equal prices the newest bid loses to preserve FIFO priority. - for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) - { - locals.participantData = state.get().participants.get(locals.participantIndex); - if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) - { - continue; - } - - if (!locals.participantData.isActive || locals.participantData.escrowedAmount == 0 || locals.participantData.requestedQuantity == 0) - { - continue; - } - - if (!locals.worstParticipantFound || locals.participantData.bidAmount < locals.worstParticipantData.bidAmount || - (locals.participantData.bidAmount == locals.worstParticipantData.bidAmount && - locals.participantData.bidIndex > locals.worstParticipantData.bidIndex)) - { - locals.worstParticipantFound = 1; - locals.worstParticipantData = locals.participantData; - locals.worstParticipantSlotIndex = locals.participantIndex; - } - } - - if (!locals.worstParticipantFound) - { - break; - } - - locals.excessQuantity = locals.activeQuantity - locals.auction.core.quantityForSale; - locals.displacedQuantity = min(locals.excessQuantity, locals.worstParticipantData.requestedQuantity); - locals.displacedRefund = smul(locals.displacedQuantity, locals.worstParticipantData.bidAmount); - locals.remainingWorstQuantity = locals.worstParticipantData.requestedQuantity - locals.displacedQuantity; - // A partial order smaller than the minimum is removed in full; keeping it would create an invalid final allocation. - if (locals.remainingWorstQuantity > 0 && locals.remainingWorstQuantity < locals.auction.core.minimumPurchaseQuantity) - { - locals.displacedQuantity = locals.worstParticipantData.requestedQuantity; - locals.displacedRefund = locals.worstParticipantData.escrowedAmount; - } - // Full displacement retires the live slot; partial displacement keeps a valid minimum-sized order active. - if (locals.displacedQuantity >= locals.worstParticipantData.requestedQuantity) - { - locals.worstParticipantData.escrowedAmount = 0; - locals.worstParticipantData.requestedQuantity = 0; - locals.worstParticipantData.allocatedQuantity = 0; - locals.worstParticipantData.isActive = 0; - locals.worstParticipantData.isWinningBid = 0; - } - else - { - locals.worstParticipantData.requestedQuantity -= locals.displacedQuantity; - locals.worstParticipantData.escrowedAmount -= locals.displacedRefund; - locals.worstParticipantData.isWinningBid = 1; - } - - if (locals.displacedRefund > 0) - { - locals.payoutInput.recipient = locals.worstParticipantData.participant; - locals.payoutInput.amount = locals.displacedRefund; - locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; - CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); - output.refundedAmount = sadd(output.refundedAmount, locals.displacedRefund); - } - locals.activeQuantity -= locals.displacedQuantity; - // Archive only retired orders; partially displaced orders remain in the live priority book. - if (!locals.worstParticipantData.isActive) - { - locals.archiveParticipantInput.participantData = locals.worstParticipantData; - CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); - locals.worstParticipantData = {}; - } - state.mut().participants.set(locals.worstParticipantSlotIndex, locals.worstParticipantData); - } - - locals.recomputeBatchHighestBidInput.auctionIndex = input.auctionIndex; - CALL(RecomputeBatchHighestBid, locals.recomputeBatchHighestBidInput, locals.recomputeBatchHighestBidOutput); - - // Small-bid service fees are retained even if the bid is later displaced. - if (locals.bidFeeCalculation.fee > 0) - { - locals.accumulateAuctionServiceFeeInput.feeAmount = locals.bidFeeCalculation.fee; - CALL(AccumulateAuctionServiceFee, locals.accumulateAuctionServiceFeeInput, locals.accumulateAuctionServiceFeeOutput); - } - - if (static_cast(qpi.invocationReward()) > locals.bidFeeCalculation.requiredReward) - { - locals.payoutInput.recipient = qpi.invocator(); - locals.payoutInput.amount = static_cast(qpi.invocationReward()) - locals.bidFeeCalculation.requiredReward; - locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; - CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); - output.refundedAmount = - sadd(output.refundedAmount, static_cast(qpi.invocationReward()) - locals.bidFeeCalculation.requiredReward); - } - - output.escrowedAmount = locals.bidFeeCalculation.escrowAmount; - output.success = 1; - } - - /** - * @brief Validates and records a Standard Auction bid, refunding replaced escrow. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(ProcessStandardBid) - { - output.escrowedAmount = 0; - output.refundedAmount = 0; - output.errorCode = EAuctionError::Success; - output.success = 0; - locals.highestBidderExists = 0; - locals.finalizeImmediately = 0; - locals.participantExists = 0; - locals.freeParticipantSlotFound = 0; - if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) - { - output.errorCode = EAuctionError::AuctionNotFound; - return; - } - - if (locals.auction.core.quantityForSale == 0 || locals.auction.core.quantityForSale < locals.auction.core.minimumPurchaseQuantity || - input.bidAmount == 0) - { - output.errorCode = EAuctionError::InvalidInput; - return; - } - // Reserve distinct entries for a replaced bidder, bidder change, three fee wallets, and the seller. - // This also guarantees that an accepted Buy Now bid can complete settlement in the same call. - if (state.get().pendingQuPayouts.population() > state.get().pendingQuPayouts.capacity() - NOST_STANDARD_BID_MAX_PAYOUT_RECIPIENTS) - { - output.errorCode = EAuctionError::PayoutQueueFull; - return; - } - - locals.requiredEscrow = input.bidAmount; - if (static_cast(qpi.invocationReward()) < locals.requiredEscrow) - { - output.errorCode = EAuctionError::InsufficientFunds; - return; - } - - if (locals.auction.core.highestBidPrice == 0) - { - if (input.bidAmount < locals.auction.core.initialPrice) - { - output.errorCode = EAuctionError::BidTooLow; - return; - } - } - else if (input.bidAmount < sadd(locals.auction.core.highestBidPrice, locals.auction.core.minimumBidIncrement)) - { - output.errorCode = EAuctionError::BidTooLow; - return; - } - - // Standard bidders update their own active slot, while a new bidder needs one reusable slot. - for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) - { - locals.participantData = state.get().participants.get(locals.participantSlotIndex); - if (locals.participantData.isUsed && locals.participantData.isActive && locals.participantData.auctionIndex == input.auctionIndex && - locals.participantData.participant == qpi.invocator()) - { - locals.participantExists = 1; - break; - } - if (!locals.freeParticipantSlotFound && !locals.participantData.isUsed) - { - locals.freeParticipantSlotFound = 1; - locals.freeParticipantSlotIndex = locals.participantSlotIndex; - } - } - locals.previousEscrow = locals.participantExists ? locals.participantData.escrowedAmount : 0; - if (!locals.participantExists && !locals.freeParticipantSlotFound) - { - output.errorCode = EAuctionError::StorageFull; - return; - } - if (!locals.participantExists) - { - locals.participantSlotIndex = locals.freeParticipantSlotIndex; - if (locals.auction.core.nextBidIndex == UINT64_MAX) - { - output.errorCode = EAuctionError::StorageFull; - return; - } - } - - locals.participantData.escrowedAmount = locals.requiredEscrow; - locals.participantData.requestedQuantity = locals.auction.core.quantityForSale; - locals.participantData.allocatedQuantity = 0; - locals.participantData.bidAmount = input.bidAmount; - locals.participantData.lastBidTime = input.currentDate; - locals.participantData.participant = qpi.invocator(); - locals.participantData.auctionIndex = input.auctionIndex; - locals.participantData.bidIndex = locals.participantExists ? locals.participantData.bidIndex : locals.auction.core.nextBidIndex; - locals.participantData.isUsed = 1; - locals.participantData.isActive = 1; - locals.participantData.isWinningBid = 0; - if (!locals.participantExists) - { - locals.auction.core.nextBidIndex = sadd(locals.auction.core.nextBidIndex, 1ULL); - } - - // A new highest bid releases the previous bidder's escrow before storing the replacement. - locals.highestBidderSlotIndex = locals.auction.core.highestBidSlotIndex; - if (locals.highestBidderSlotIndex < state.get().participants.capacity()) - { - locals.previousHighestBidderData = state.get().participants.get(locals.highestBidderSlotIndex); - locals.highestBidderExists = locals.previousHighestBidderData.isUsed && locals.previousHighestBidderData.isActive && - locals.previousHighestBidderData.auctionIndex == input.auctionIndex; - } - if (locals.highestBidderExists && locals.previousHighestBidderData.participant != qpi.invocator()) - { - locals.payoutInput.recipient = locals.previousHighestBidderData.participant; - locals.payoutInput.amount = locals.previousHighestBidderData.escrowedAmount; - locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; - CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); - output.refundedAmount = sadd(output.refundedAmount, locals.previousHighestBidderData.escrowedAmount); - locals.previousHighestBidderData.escrowedAmount = 0; - locals.previousHighestBidderData.requestedQuantity = 0; - locals.previousHighestBidderData.isActive = 0; - locals.previousHighestBidderData.isWinningBid = 0; - locals.archiveParticipantInput.participantData = locals.previousHighestBidderData; - CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); - locals.previousHighestBidderData = {}; - state.mut().participants.set(locals.highestBidderSlotIndex, locals.previousHighestBidderData); - } - - locals.participantData.isWinningBid = 1; - locals.auction.core.highestBidder = qpi.invocator(); - locals.auction.core.highestBidPrice = input.bidAmount; - locals.auction.core.highestBidQuantity = locals.auction.core.quantityForSale; - locals.auction.core.highestBidAmount = locals.requiredEscrow; - locals.auction.core.highestBidSlotIndex = locals.participantSlotIndex; - - locals.auction.core.lastBidAt = input.currentDate; - if ((locals.auction.core.auctionDurationSeconds - input.elapsedSeconds) <= NOST_AUCTION_EXTENSION_SECONDS) - { - locals.auction.core.auctionDurationSeconds = sadd(locals.auction.core.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); - } - if (locals.auction.core.buyNowPrice > 0 && input.bidAmount >= locals.auction.core.buyNowPrice) - { - locals.finalizeImmediately = 1; - } - - state.mut().participants.set(locals.participantSlotIndex, locals.participantData); - state.mut().auctionList.replace(input.auctionIndex, locals.auction); - - // Refund replaced self-escrow and excess reward after the new bid state is durable. - if (locals.previousEscrow > 0) - { - locals.payoutInput.recipient = qpi.invocator(); - locals.payoutInput.amount = locals.previousEscrow; - locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; - CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); - output.refundedAmount = sadd(output.refundedAmount, locals.previousEscrow); - } - if (static_cast(qpi.invocationReward()) > locals.requiredEscrow) - { - locals.payoutInput.recipient = qpi.invocator(); - locals.payoutInput.amount = static_cast(qpi.invocationReward()) - locals.requiredEscrow; - locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; - CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); - output.refundedAmount = sadd(output.refundedAmount, static_cast(qpi.invocationReward()) - locals.requiredEscrow); - } - - output.escrowedAmount = locals.requiredEscrow; - output.success = 1; - - // Buy Now closes the auction in the same procedure after the winning bid is recorded. - if (locals.finalizeImmediately) - { - locals.finalizeStandardAuctionInput.auctionIndex = input.auctionIndex; - locals.finalizeStandardAuctionInput.currentDate = input.currentDate; - CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); - } - } - - /** - * @brief Validates the fixed-size IPFS metadata CID field. - */ - PRIVATE_FUNCTION_WITH_LOCALS(ValidateMetadataCid) - { - output.isValid = 0; - locals.hasPayloadCharacters = 0; - locals.reachedTerminator = 0; - - // Nostromo stores lowercase base32 CIDv1 values, which begin with the multibase prefix `b`. - if (input.metadataIpfsCid.get(0) != QPI::Ch::b) - { - return; - } - - // After the first zero byte, the fixed-size CID field must remain zero-padded. - for (locals.cidIndex = 1; locals.cidIndex < input.metadataIpfsCid.capacity(); ++locals.cidIndex) - { - locals.cidChar = input.metadataIpfsCid.get(locals.cidIndex); - if (locals.cidChar == 0) - { - locals.reachedTerminator = 1; - continue; - } - - if (locals.reachedTerminator) - { - return; - } - - if ((locals.cidChar >= QPI::Ch::a && locals.cidChar <= QPI::Ch::z) || (locals.cidChar >= QPI::Ch::_2 && locals.cidChar <= QPI::Ch::_7)) - { - locals.hasPayloadCharacters = 1; - continue; - } - - return; - } - - if (!locals.hasPayloadCharacters) - { - return; - } - - output.isValid = 1; - } - - /** - * @brief Verifies that the invocator can escrow every non-empty lot asset. - */ - PRIVATE_FUNCTION_WITH_LOCALS(VerifyAuctionLotBalances) - { - output.hasEnoughBalance = 1; - // Creation validates possession before attempting escrow so failures can refund without rollback. - for (locals.lotItemIndex = 0; locals.lotItemIndex < input.auctionLotItems.capacity(); ++locals.lotItemIndex) - { - locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); - if (isZeroAsset(locals.lotItem.asset) || locals.lotItem.quantity <= 0) - { - continue; - } - - locals.possessedShares = qpi.numberOfPossessedShares(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, qpi.invocator(), - qpi.invocator(), SELF_INDEX, SELF_INDEX); - if (locals.possessedShares < locals.lotItem.quantity) - { - output.hasEnoughBalance = 0; - return; - } - } - } - - /** - * @brief Returns escrowed lot assets to the specified recipient. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(RollbackAuctionLotAssets) - { - // Rollback is shared by cancellation, failed creation, rejected standard sales, and no-sale finalization. - for (locals.lotItemIndex = 0; locals.lotItemIndex < input.auctionLotItems.capacity(); ++locals.lotItemIndex) - { - locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); - if (isZeroAsset(locals.lotItem.asset) || locals.lotItem.quantity <= 0) - { - continue; - } - qpi.transferShareOwnershipAndPossession(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, SELF, SELF, locals.lotItem.quantity, - input.recipient); - } - } - - /** - * @brief Settles a Batch Auction by allocating winning quantities and closing the auction. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(FinalizeBatchAuction) - { - output.success = 0; - locals.bestParticipantFound = 0; - locals.lotItemFound = 0; - locals.soldQuantity = 0; - locals.totalGrossAmount = 0; - - // Abort if the auction no longer exists or is no longer an active batch auction. - if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) - { - return; - } - - if (locals.auction.core.type != EAuctionType::Batch || locals.auction.core.status != EAuctionStatus::Active) - { - return; - } - if (state.get().pendingQuPayouts.population() > - state.get().pendingQuPayouts.capacity() - state.get().participants.capacity() - NOST_AUCTION_REVENUE_MAX_PAYOUT_RECIPIENTS) - { - return; - } - - // Resolve the single sellable lot entry that represents the batch asset and quantity in escrow. - for (locals.lotItemIndex = 0; locals.lotItemIndex < locals.auction.core.auctionLotItems.capacity(); ++locals.lotItemIndex) - { - locals.batchLotItem = locals.auction.core.auctionLotItems.get(locals.lotItemIndex); - if (!isZeroAsset(locals.batchLotItem.asset) && locals.batchLotItem.quantity > 0) - { - locals.lotItemFound = 1; - break; - } - } - if (!locals.lotItemFound) - { - return; - } - - // Stop before producing a fragment below the auction minimum; the remainder stays with the seller. - locals.remainingQuantity = locals.auction.core.quantityForSale; - while (locals.remainingQuantity >= locals.auction.core.minimumPurchaseQuantity) - { - locals.bestParticipantFound = 0; - locals.participantIndex = 0; - - // Price priority is descending; the monotonic bid index is the only FIFO tie-breaker. - while (locals.participantIndex < state.get().participants.capacity()) - { - locals.participantData = state.get().participants.get(locals.participantIndex); - if (locals.participantData.isUsed && locals.participantData.auctionIndex == input.auctionIndex) - { - if (locals.participantData.isActive && locals.participantData.escrowedAmount > 0) - { - if (!locals.bestParticipantFound || locals.participantData.bidAmount > locals.bestParticipantData.bidAmount || - (locals.participantData.bidAmount == locals.bestParticipantData.bidAmount && - locals.participantData.bidIndex < locals.bestParticipantData.bidIndex)) - { - locals.bestParticipantFound = 1; - locals.bestParticipantData = locals.participantData; - locals.bestParticipantSlotIndex = locals.participantIndex; - } - } - } - ++locals.participantIndex; - } - - if (!locals.bestParticipantFound) - { - break; - } - - // Price the winning allocation and compute any escrow surplus that must be returned immediately. - locals.allocatedQuantity = min(locals.remainingQuantity, locals.bestParticipantData.requestedQuantity); - locals.requiredPayment = smul(locals.allocatedQuantity, locals.bestParticipantData.bidAmount); - locals.refundAmount = 0; - if (locals.bestParticipantData.escrowedAmount > locals.requiredPayment) - { - locals.refundAmount = locals.bestParticipantData.escrowedAmount - locals.requiredPayment; - } - - // Transfer the awarded shares, mark the participant as a winner, and advance settlement totals. - if (locals.allocatedQuantity > 0) - { - qpi.transferShareOwnershipAndPossession(locals.batchLotItem.asset.assetName, locals.batchLotItem.asset.issuer, SELF, SELF, - locals.allocatedQuantity, locals.bestParticipantData.participant); - locals.bestParticipantData.allocatedQuantity = locals.allocatedQuantity; - locals.bestParticipantData.isWinningBid = 1; - locals.soldQuantity = sadd(locals.soldQuantity, locals.allocatedQuantity); - locals.totalGrossAmount = sadd(locals.totalGrossAmount, locals.requiredPayment); - locals.remainingQuantity -= locals.allocatedQuantity; - } - - // Return the unused part of the winner escrow when the participant requested more than the remaining supply. - if (locals.refundAmount > 0) - { - locals.payoutInput.recipient = locals.bestParticipantData.participant; - locals.payoutInput.amount = locals.refundAmount; - locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; - CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); - if (!locals.payoutOutput.success) - { - return; - } - } - - // Archive the completed bid and release its active slot immediately. - locals.bestParticipantData.escrowedAmount = 0; - locals.bestParticipantData.isActive = 0; - locals.archiveParticipantInput.participantData = locals.bestParticipantData; - CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); - locals.bestParticipantData = {}; - state.mut().participants.set(locals.bestParticipantSlotIndex, locals.bestParticipantData); - } - - // Refund every non-winning or non-allocated bid that still has escrow locked after winner selection. - locals.participantIndex = 0; - while (locals.participantIndex < state.get().participants.capacity()) - { - locals.participantData = state.get().participants.get(locals.participantIndex); - if (locals.participantData.isUsed && locals.participantData.auctionIndex == input.auctionIndex) - { - if (locals.participantData.escrowedAmount > 0) - { - locals.payoutInput.recipient = locals.participantData.participant; - locals.payoutInput.amount = locals.participantData.escrowedAmount; - locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; - CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); - if (!locals.payoutOutput.success) - { - return; - } - locals.participantData.escrowedAmount = 0; - locals.participantData.allocatedQuantity = 0; - locals.participantData.isWinningBid = 0; - } - locals.participantData.isActive = 0; - locals.archiveParticipantInput.participantData = locals.participantData; - CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); - locals.participantData = {}; - state.mut().participants.set(locals.participantIndex, locals.participantData); - } - ++locals.participantIndex; - } - - // Return any unsold batch quantity to the seller when demand did not consume the entire lot. - if (locals.soldQuantity < locals.auction.core.quantityForSale) - { - qpi.transferShareOwnershipAndPossession(locals.batchLotItem.asset.assetName, locals.batchLotItem.asset.issuer, SELF, SELF, - locals.auction.core.quantityForSale - locals.soldQuantity, locals.auction.core.seller); - } - - // Split the collected proceeds according to Nostromo auction fee rules and pay the seller net amount. - locals.distributeAuctionRevenueInput.seller = locals.auction.core.seller; - locals.distributeAuctionRevenueInput.grossAmount = locals.totalGrossAmount; - CALL(DistributeAuctionRevenue, locals.distributeAuctionRevenueInput, locals.distributeAuctionRevenueOutput); - if (!locals.distributeAuctionRevenueOutput.success) - { - return; - } - - // Persist the final sold quantity and close the auction as settled. - locals.auction.core.allocatedQuantity = locals.soldQuantity; - locals.auction.core.status = EAuctionStatus::Finalized; - locals.auction.core.settledAt = input.currentDate; - locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; - state.mut().totalFinalizedAuctions = sadd(state.get().totalFinalizedAuctions, 1ULL); - locals.archiveClosedAuctionInput.auction = locals.auction; - CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); - output.success = 1; - } - - /** - * @brief Settles a Standard Auction by transferring the lot or returning it to the seller. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(FinalizeStandardAuction) - { - output.success = 0; - locals.highestBidderExists = 0; - locals.lotSold = 0; - - if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) - { - return; - } - - if (locals.auction.core.type != EAuctionType::Standard) - { - return; - } - if (state.get().pendingQuPayouts.population() > state.get().pendingQuPayouts.capacity() - NOST_STANDARD_FINALIZATION_MAX_PAYOUT_RECIPIENTS) - { - return; - } - - locals.highestBidderSlotIndex = locals.auction.core.highestBidSlotIndex; - if (locals.highestBidderSlotIndex < state.get().participants.capacity()) - { - locals.highestBidderData = state.get().participants.get(locals.highestBidderSlotIndex); - locals.highestBidderExists = - locals.highestBidderData.isUsed && locals.highestBidderData.isActive && locals.highestBidderData.auctionIndex == input.auctionIndex; - } - - // A valid highest bid transfers the whole standard lot and treats escrow as gross proceeds. - if (locals.highestBidderExists && locals.highestBidderData.escrowedAmount > 0) - { - locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; - locals.rollbackAuctionLotAssetsInput.recipient = locals.highestBidderData.participant; - CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); - - locals.distributeAuctionRevenueInput.seller = locals.auction.core.seller; - locals.distributeAuctionRevenueInput.grossAmount = locals.highestBidderData.escrowedAmount; - CALL(DistributeAuctionRevenue, locals.distributeAuctionRevenueInput, locals.distributeAuctionRevenueOutput); - if (!locals.distributeAuctionRevenueOutput.success) - { - return; - } - - locals.highestBidderData.allocatedQuantity = locals.auction.core.quantityForSale; - locals.highestBidderData.isWinningBid = 1; - locals.highestBidderData.escrowedAmount = 0; - locals.highestBidderData.isActive = 0; - locals.archiveParticipantInput.participantData = locals.highestBidderData; - CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); - locals.highestBidderData = {}; - state.mut().participants.set(locals.highestBidderSlotIndex, locals.highestBidderData); - locals.auction.core.allocatedQuantity = locals.auction.core.quantityForSale; - locals.lotSold = 1; - } - else - { - // No active funded bid means the seller receives the lot back with no revenue distribution. - locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; - locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.core.seller; - CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); - locals.auction.core.allocatedQuantity = 0; - } - - // Closed standard auctions retain winner fields only when the lot actually sold. - locals.auction.core.status = EAuctionStatus::Finalized; - locals.auction.core.settledAt = input.currentDate; - if (!locals.lotSold) - { - locals.auction.core.highestBidAmount = 0; - locals.auction.core.highestBidPrice = 0; - locals.auction.core.highestBidQuantity = 0; - locals.auction.core.highestBidder = NULL_ID; - } - locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; - state.mut().totalFinalizedAuctions = sadd(state.get().totalFinalizedAuctions, 1ULL); - locals.archiveClosedAuctionInput.auction = locals.auction; - CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); - output.success = 1; - } - - /** - * @brief Rejects a pending Standard Auction bid and closes the auction without a sale. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(RejectStandardAuction) - { - output.refundedAmount = 0; - output.success = 0; - locals.highestBidderExists = 0; - - if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) - { - return; - } - - if (locals.auction.core.type != EAuctionType::Standard || locals.auction.core.status != EAuctionStatus::PendingSellerDecision) - { - return; - } - if (state.get().pendingQuPayouts.population() == state.get().pendingQuPayouts.capacity()) - { - return; - } - - locals.highestBidderSlotIndex = locals.auction.core.highestBidSlotIndex; - if (locals.highestBidderSlotIndex < state.get().participants.capacity()) - { - locals.highestBidderData = state.get().participants.get(locals.highestBidderSlotIndex); - locals.highestBidderExists = - locals.highestBidderData.isUsed && locals.highestBidderData.isActive && locals.highestBidderData.auctionIndex == input.auctionIndex; - } - - // Seller rejection unwinds the pending bid instead of distributing its escrow as proceeds. - if (locals.highestBidderExists && locals.highestBidderData.escrowedAmount > 0) - { - locals.payoutInput.recipient = locals.highestBidderData.participant; - locals.payoutInput.amount = locals.highestBidderData.escrowedAmount; - locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; - CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); - if (!locals.payoutOutput.success) - { - return; - } - output.refundedAmount = locals.highestBidderData.escrowedAmount; - locals.highestBidderData.escrowedAmount = 0; - locals.highestBidderData.allocatedQuantity = 0; - locals.highestBidderData.isActive = 0; - locals.highestBidderData.isWinningBid = 0; - locals.archiveParticipantInput.participantData = locals.highestBidderData; - CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); - locals.highestBidderData = {}; - state.mut().participants.set(locals.highestBidderSlotIndex, locals.highestBidderData); - } - - // The seller keeps the lot after rejection, and the auction is closed as finalized. - locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; - locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.core.seller; - CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); - - locals.auction.core.allocatedQuantity = 0; - locals.auction.core.highestBidAmount = 0; - locals.auction.core.highestBidPrice = 0; - locals.auction.core.highestBidQuantity = 0; - locals.auction.core.highestBidder = NULL_ID; - locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; - locals.auction.core.status = EAuctionStatus::Finalized; - locals.auction.core.settledAt = input.currentDate; - state.mut().totalFinalizedAuctions = sadd(state.get().totalFinalizedAuctions, 1ULL); - locals.archiveClosedAuctionInput.auction = locals.auction; - CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); - output.success = 1; - } - - /** - * @brief Transfers auction lot assets into contract escrow during creation. - */ - PRIVATE_PROCEDURE_WITH_LOCALS(EscrowAuctionLotAssets) - { - output.success = 1; - // Escrow entries one by one; a later failure rolls back earlier successful transfers. - for (locals.lotItemIndex = 0; locals.lotItemIndex < input.auctionLotItems.capacity(); ++locals.lotItemIndex) - { - locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); - if (isZeroAsset(locals.lotItem.asset) || locals.lotItem.quantity <= 0) - { - continue; - } - - locals.remainingShares = qpi.transferShareOwnershipAndPossession(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, - qpi.invocator(), qpi.invocator(), locals.lotItem.quantity, SELF); - if (locals.remainingShares < 0) - { - // `transferShareOwnershipAndPossession` returns the remaining number of matching shares after a successful transfer. - // Negative values mean the transfer failed without moving the requested lot entry. - for (locals.rollbackLotItemIndex = 0; locals.rollbackLotItemIndex < locals.lotItemIndex; ++locals.rollbackLotItemIndex) - { - locals.lotItem = input.auctionLotItems.get(locals.rollbackLotItemIndex); - if (isZeroAsset(locals.lotItem.asset) || locals.lotItem.quantity <= 0) - { - continue; - } - qpi.transferShareOwnershipAndPossession(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, SELF, SELF, - locals.lotItem.quantity, qpi.invocator()); - } - output.success = 0; - return; - } - } - } - - /** - * @brief Creates a new Batch Auction or Standard Auction in the Nostromo Auction House. - * @note `CreateAuction_input` defines the IPFS metadata CID stored through Pinata, the auction lot, pricing, duration, and visibility rules. - * @note Batch auctions require `minimumPurchaseQuantity` in the range `[1, quantityForSale]`; standard auctions ignore it and store zero. - * @note A successful public Batch or Standard Auction accumulates the configured public creation fee, distributed at `END_EPOCH`. - * Insufficient payment rejects creation, overpayment is refunded, and failed creation refunds the full reward. - * @note Private auctions require the configured private auction fee, which is accumulated and distributed at `END_EPOCH` between shareholders - * and the configured fee recipients, and must use at least one access mode. If both modes are configured, either one grants access. - */ - PUBLIC_PROCEDURE_WITH_LOCALS(CreateAuction) - { - output.errorCode = EAuctionError::InvalidInput; - - // Any rejection before escrow succeeds refunds the full invocation reward. - CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); - if (locals.isAuctionInteractionPausedOutput.isPaused) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::AuctionPaused; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, - qpi.invocationReward()); - logProcedureResult(locals.log); - return; - } - - if (!isSupportedAuctionType(static_cast(input.auctionType))) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::InvalidAuctionType; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, - qpi.invocationReward()); - logProcedureResult(locals.log); - return; - } - - if (!isSupportedAuctionVisibility(static_cast(input.auctionVisibility))) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::InvalidVisibility; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, - qpi.invocationReward()); - logProcedureResult(locals.log); - return; - } - - if (state.get().auctionList.population() >= state.get().auctionList.capacity()) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::StorageFull; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, - qpi.invocationReward()); - logProcedureResult(locals.log); - return; - } - - if (state.get().totalAuctionsCreated == UINT64_MAX) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::AuctionIndexExhausted; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, - qpi.invocationReward()); - logProcedureResult(locals.log); - return; - } - - locals.validateMetadataCidInput.metadataIpfsCid = input.metadataIpfsCid; - CALL(ValidateMetadataCid, locals.validateMetadataCidInput, locals.validateMetadataCidOutput); - if (!locals.validateMetadataCidOutput.isValid) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, - qpi.invocationReward()); - logProcedureResult(locals.log); - return; - } - - locals.analyzeAuctionLotInput.auctionLotItems = input.auctionLotItems; - locals.analyzeAuctionLotInput.durationDays = input.durationDays; - CALL(AnalyzeAuctionLot, locals.analyzeAuctionLotInput, locals.analyzeAuctionLotOutput); - if (!locals.analyzeAuctionLotOutput.isValid) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, - qpi.invocationReward()); - logProcedureResult(locals.log); - return; - } - // Resolve auction-type-specific quantity and price invariants before touching assets. - locals.resolvedQuantityForSale = 0; - locals.resolvedMinimumPurchaseQuantity = 0; - switch (static_cast(input.auctionType)) - { - case EAuctionType::Batch: - - if (!resolveBatchAuctionCreateParams(locals.analyzeAuctionLotOutput.lotItemCount, locals.analyzeAuctionLotOutput.totalEscrowQuantity, - input.minimumPurchaseQuantity, locals.resolvedQuantityForSale, - locals.resolvedMinimumPurchaseQuantity, input.buyNowPrice)) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, - qpi.invocationReward()); - logProcedureResult(locals.log); - return; - } - break; - case EAuctionType::Standard: - if (!resolveStandardAuctionCreateParams(input.minimumBidIncrement, locals.resolvedQuantityForSale, - locals.resolvedMinimumPurchaseQuantity, input.buyNowPrice, input.initialPrice, - input.salePrice)) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, - qpi.invocationReward()); - logProcedureResult(locals.log); - return; - } - break; - default: - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::InvalidAuctionType; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, - qpi.invocationReward()); - logProcedureResult(locals.log); - return; - } - - // Private auctions require at least one access gate and may combine wallet and asset access. - locals.countAllowedBidderWalletsInput.allowedBidderWallets = input.allowedBidderWallets; - CALL(CountAllowedBidderWallets, locals.countAllowedBidderWalletsInput, locals.countAllowedBidderWalletsOutput); - locals.countRequiredAccessAssetsInput.requiredAccessAssets = input.requiredAccessAssets; - CALL(CountRequiredAccessAssets, locals.countRequiredAccessAssetsInput, locals.countRequiredAccessAssetsOutput); - if (!locals.countRequiredAccessAssetsOutput.isValid || - !validatePrivateAuctionAccess(static_cast(input.auctionVisibility), - locals.countRequiredAccessAssetsOutput.requiredAccessAssetCount, - locals.countAllowedBidderWalletsOutput.allowedWalletCount)) + if(locals.curDate > locals.startDate || locals.startDate >= locals.endDate || checkValidNostDateTime(locals.startDate) == 0 || checkValidNostDateTime(locals.endDate) == 0) { + output.indexOfProject = NOSTROMO_MAX_NUMBER_PROJECT; if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, - qpi.invocationReward()); - logProcedureResult(locals.log); return; } - locals.requiredFee = getCreateAuctionFee(static_cast(input.auctionVisibility), state); - if (qpi.invocationReward() < locals.requiredFee) + if (state.get().tokens.contains(input.tokenName)) { + output.indexOfProject = NOSTROMO_MAX_NUMBER_PROJECT; if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = EAuctionError::InsufficientFunds; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, - qpi.invocationReward()); - logProcedureResult(locals.log); - return; + return ; } - locals.verifyAuctionLotBalancesInput.auctionLotItems = input.auctionLotItems; - CALL(VerifyAuctionLotBalances, locals.verifyAuctionLotBalancesInput, locals.verifyAuctionLotBalancesOutput); - if (!locals.verifyAuctionLotBalancesOutput.hasEnoughBalance) + if (state.get().users.get(qpi.invocator(), locals.tierLevel) && (locals.tierLevel == 4 || locals.tierLevel == 5)) { - if (qpi.invocationReward() > 0) + if (qpi.invocationReward() < NOSTROMO_CREATE_PROJECT_FEE) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.indexOfProject = NOSTROMO_MAX_NUMBER_PROJECT; + return ; } - output.errorCode = EAuctionError::InsufficientAssetBalance; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, - qpi.invocationReward()); - logProcedureResult(locals.log); - return; - } - - // From this point onward, asset escrow may need explicit rollback on storage failure. - locals.escrowAuctionLotAssetsInput.auctionLotItems = input.auctionLotItems; - CALL(EscrowAuctionLotAssets, locals.escrowAuctionLotAssetsInput, locals.escrowAuctionLotAssetsOutput); - if (!locals.escrowAuctionLotAssetsOutput.success) - { - if (qpi.invocationReward() > 0) + if (qpi.invocationReward() > NOSTROMO_CREATE_PROJECT_FEE) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + qpi.transfer(qpi.invocator(), qpi.invocationReward() - NOSTROMO_CREATE_PROJECT_FEE); } - output.errorCode = EAuctionError::InsufficientAssetBalance; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, - qpi.invocationReward()); - logProcedureResult(locals.log); - return; - } + state.mut().epochRevenue += NOSTROMO_CREATE_PROJECT_FEE; - locals.auction.core.auctionIndex = state.get().totalAuctionsCreated; - locals.auction.core.quantityForSale = locals.resolvedQuantityForSale; - locals.auction.core.minimumPurchaseQuantity = locals.resolvedMinimumPurchaseQuantity; - locals.auction.core.initialPrice = input.initialPrice; - locals.auction.core.salePrice = input.salePrice; - locals.auction.core.minimumBidIncrement = input.minimumBidIncrement; - locals.auction.core.buyNowPrice = input.buyNowPrice; - locals.auction.core.auctionDurationSeconds = smul(static_cast(input.durationDays), NOST_SECONDS_PER_DAY); - locals.auction.core.createdAt = qpi.now(); - locals.auction.core.lastBidAt = locals.auction.core.createdAt; - locals.auction.core.seller = qpi.invocator(); - locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; - // Duplicate required access assets collapse to the highest configured quantity. - for (locals.requiredAccessAssetIndex = 0; locals.requiredAccessAssetIndex < input.requiredAccessAssets.capacity(); - ++locals.requiredAccessAssetIndex) - { - locals.requiredAccessAsset = input.requiredAccessAssets.get(locals.requiredAccessAssetIndex); - if (!isZeroAsset(locals.requiredAccessAsset.asset) && - (!locals.auction.requiredAccessAssets.get(locals.requiredAccessAsset.asset, locals.existingRequiredAccessQuantity) || - locals.requiredAccessAsset.quantity > locals.existingRequiredAccessQuantity)) - { - locals.auction.requiredAccessAssets.set(locals.requiredAccessAsset.asset, locals.requiredAccessAsset.quantity); - } - } - locals.auction.core.auctionLotItems = input.auctionLotItems; - for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < input.allowedBidderWallets.capacity(); ++locals.allowedWalletIndex) - { - if (!isZero(input.allowedBidderWallets.get(locals.allowedWalletIndex))) - { - locals.auction.allowedBidderWallets.add(input.allowedBidderWallets.get(locals.allowedWalletIndex)); - } - } - locals.auction.core.metadataIpfsCid = input.metadataIpfsCid; - locals.auction.core.type = static_cast(input.auctionType); - locals.auction.core.visibility = static_cast(input.auctionVisibility); - locals.auction.core.status = EAuctionStatus::Active; + locals.newProject.creator = qpi.invocator(); + locals.newProject.tokenName = input.tokenName; + locals.newProject.supplyOfToken = input.supply; + locals.newProject.startDate = locals.startDate; + locals.newProject.endDate = locals.endDate; + locals.newProject.numberOfYes = 0; + locals.newProject.numberOfNo = 0; - // If persistent auction storage fails after escrow, return the lot before refunding the fee reward. - if (state.mut().auctionList.set(locals.auction.core.auctionIndex, locals.auction) == NULL_INDEX) + output.indexOfProject = state.get().numberOfCreatedProject; + state.mut().projects.set(state.get().numberOfCreatedProject, locals.newProject); + state.mut().numberOfCreatedProject++; + state.mut().tokens.add(input.tokenName); + } + else { - locals.rollbackAuctionLotAssetsInput.auctionLotItems = input.auctionLotItems; - locals.rollbackAuctionLotAssetsInput.recipient = qpi.invocator(); - CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = EAuctionError::StorageFull; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, - qpi.invocationReward()); - logProcedureResult(locals.log); - return; - } - - // Creation fees are held until END_EPOCH; overpayment is returned immediately. - if (locals.requiredFee > 0) - { - locals.accumulateAuctionServiceFeeInput.feeAmount = static_cast(locals.requiredFee); - CALL(AccumulateAuctionServiceFee, locals.accumulateAuctionServiceFeeInput, locals.accumulateAuctionServiceFeeOutput); - } - - if (qpi.invocationReward() > locals.requiredFee) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.requiredFee); + output.indexOfProject = NOSTROMO_MAX_NUMBER_PROJECT; } - - output.auctionIndex = locals.auction.core.auctionIndex; - state.mut().totalAuctionsCreated = sadd(state.get().totalAuctionsCreated, 1ULL); - output.errorCode = EAuctionError::Success; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, qpi.invocationReward()); - logProcedureResult(locals.log); } - /** - * @brief Places a bid in an active auction. - * @note Batch auctions interpret `bidAmount` as price per asset and reject requested `quantity` below `minimumPurchaseQuantity` with a full - * refund. - * @note An accepted Batch bid escrows `quantity * bidAmount` and accumulates `max(100 - quantity * bidAmount, 0)` qu for distribution at - * `END_EPOCH`. Excess reward is refunded; rejected bids refund the full reward. The accumulated fee is not refunded if the bid is later - * displaced. - * @note Batch final allocations are also at least `minimumPurchaseQuantity`; smaller unsold remainders return to the seller and affected bids are - * fully refunded. - * @note Standard auctions interpret `bidAmount` as the total price for the whole lot and ignore `quantity`. - */ - PUBLIC_PROCEDURE_WITH_LOCALS(PlaceBid) + struct voteInProject_locals { - output.errorCode = EAuctionError::InvalidInput; - - // Common auction gates run before type-specific bid processing; failed gates refund the reward. - CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); - if (locals.isAuctionInteractionPausedOutput.isPaused) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::AuctionPaused; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); - logProcedureResult(locals.log); - return; - } + projectInfo votedProject; + Array votedList; + uint32 elementIndex, curDate, numberOfVotedProject, i; + bit flag; + }; - if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + PUBLIC_PROCEDURE_WITH_LOCALS(voteInProject) + { + if (input.indexOfProject >= state.get().numberOfCreatedProject) { - locals.findAuctionInput.auctionIndex = input.auctionIndex; - CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = locals.findAuctionOutput.found ? EAuctionError::AuctionClosed : EAuctionError::AuctionNotFound; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); - logProcedureResult(locals.log); - return; + return ; } - - if (locals.auction.core.status != EAuctionStatus::Active) + if (state.get().users.contains(qpi.invocator()) == 0) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::AuctionClosed; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); - logProcedureResult(locals.log); - return; + return ; } - - if (locals.auction.core.seller == qpi.invocator()) + state.get().numberOfVotedProject.get(qpi.invocator(), locals.numberOfVotedProject); + if (locals.numberOfVotedProject == NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::Forbidden; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); - logProcedureResult(locals.log); - return; + return ; } - - locals.currentDate = qpi.now(); - diffDateInSecond(locals.auction.core.createdAt, locals.currentDate, locals.elapsedSeconds); - if (locals.elapsedSeconds >= locals.auction.core.auctionDurationSeconds) + state.get().voteStatus.get(qpi.invocator(), locals.votedList); + for (locals.i = 0; locals.i < locals.numberOfVotedProject; locals.i++) { - if (qpi.invocationReward() > 0) + if (locals.votedList.get(locals.i) == input.indexOfProject) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return ; } - output.errorCode = EAuctionError::AuctionClosed; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); - logProcedureResult(locals.log); - return; } - - // When both gates are configured, satisfying either one grants access. - if (locals.auction.core.visibility == EAuctionVisibility::Private) + packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); + if (locals.curDate >= state.get().projects.get(input.indexOfProject).startDate && locals.curDate < state.get().projects.get(input.indexOfProject).endDate) { - locals.hasAccess = locals.auction.allowedBidderWallets.population() > 0 && locals.auction.allowedBidderWallets.contains(qpi.invocator()); - if (!locals.hasAccess && locals.auction.requiredAccessAssets.population() > 0) + locals.votedProject = state.get().projects.get(input.indexOfProject); + if (input.decision) { - locals.hasRequiredAccessAssetInput.auctionIndex = input.auctionIndex; - CALL(HasRequiredAccessAsset, locals.hasRequiredAccessAssetInput, locals.hasRequiredAccessAssetOutput); - locals.hasAccess = locals.hasRequiredAccessAssetOutput.hasRequiredAccessAsset; + locals.votedProject.numberOfYes++; } - - if (!locals.hasAccess) + else { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::PrivateAuctionAccessDenied; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, - output.escrowedAmount); - logProcedureResult(locals.log); - return; + locals.votedProject.numberOfNo++; } + state.mut().projects.set(input.indexOfProject, locals.votedProject); + locals.votedList.set(locals.numberOfVotedProject++, input.indexOfProject); + state.mut().voteStatus.set(qpi.invocator(), locals.votedList); + state.mut().numberOfVotedProject.set(qpi.invocator(), locals.numberOfVotedProject); } - - // Type-specific processors own escrow/refund details once common validation succeeds. - switch (locals.auction.core.type) - { - case EAuctionType::Batch: - locals.processBatchBidInput.auctionIndex = input.auctionIndex; - locals.processBatchBidInput.effectiveQuantity = input.quantity; - locals.processBatchBidInput.bidAmount = input.bidAmount; - locals.processBatchBidInput.currentDate = locals.currentDate; - locals.processBatchBidInput.elapsedSeconds = locals.elapsedSeconds; - CALL(ProcessBatchBid, locals.processBatchBidInput, locals.processBatchBidOutput); - if (!locals.processBatchBidOutput.success) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.refundedAmount = locals.processBatchBidOutput.refundedAmount; - output.errorCode = locals.processBatchBidOutput.errorCode; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, - output.escrowedAmount); - logProcedureResult(locals.log); - return; - } - output.refundedAmount = sadd(output.refundedAmount, locals.processBatchBidOutput.refundedAmount); - output.escrowedAmount = locals.processBatchBidOutput.escrowedAmount; - break; - case EAuctionType::Standard: - locals.processStandardBidInput.auctionIndex = input.auctionIndex; - locals.processStandardBidInput.bidAmount = input.bidAmount; - locals.processStandardBidInput.currentDate = locals.currentDate; - locals.processStandardBidInput.elapsedSeconds = locals.elapsedSeconds; - CALL(ProcessStandardBid, locals.processStandardBidInput, locals.processStandardBidOutput); - if (!locals.processStandardBidOutput.success) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = locals.processStandardBidOutput.errorCode; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, - output.escrowedAmount); - logProcedureResult(locals.log); - return; - } - output.refundedAmount = sadd(output.refundedAmount, locals.processStandardBidOutput.refundedAmount); - output.escrowedAmount = locals.processStandardBidOutput.escrowedAmount; - break; - default: - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::InvalidAuctionType; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, - output.escrowedAmount); - logProcedureResult(locals.log); - return; - } - output.errorCode = EAuctionError::Success; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); - logProcedureResult(locals.log); } - /** - * @brief Cancels an active auction before the first accepted bid is placed. - * @note Once any bid is accepted, the seller can no longer cancel the auction. - * @note The cancellation fee is based on the configured reserve price for the full batch quantity or standard lot and is distributed between - * shareholders and the configured fee recipients. - */ - PUBLIC_PROCEDURE_WITH_LOCALS(CancelAuction) + struct createFundraising_locals + { + projectInfo tmpProject; + fundaraisingInfo newFundraising; + uint32 curDate, firstPhaseStartDate, firstPhaseEndDate, secondPhaseStartDate, secondPhaseEndDate, thirdPhaseStartDate, thirdPhaseEndDate, listingStartDate, cliffEndDate, vestingEndDate; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(createFundraising) { - output.refundedAmount = 0; - output.cancellationFee = 0; - output.errorCode = EAuctionError::InvalidInput; + packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); + packNostromoDate(input.firstPhaseStartYear, input.firstPhaseStartMonth, input.firstPhaseStartDay, input.firstPhaseStartHour, 0, 0, locals.firstPhaseStartDate); + packNostromoDate(input.secondPhaseStartYear, input.secondPhaseStartMonth, input.secondPhaseStartDay, input.secondPhaseStartHour, 0, 0, locals.secondPhaseStartDate); + packNostromoDate(input.thirdPhaseStartYear, input.thirdPhaseStartMonth, input.thirdPhaseStartDay, input.thirdPhaseStartHour, 0, 0, locals.thirdPhaseStartDate); + packNostromoDate(input.firstPhaseEndYear, input.firstPhaseEndMonth, input.firstPhaseEndDay, input.firstPhaseEndHour, 0, 0, locals.firstPhaseEndDate); + packNostromoDate(input.secondPhaseEndYear, input.secondPhaseEndMonth, input.secondPhaseEndDay, input.secondPhaseEndHour, 0, 0, locals.secondPhaseEndDate); + packNostromoDate(input.thirdPhaseEndYear, input.thirdPhaseEndMonth, input.thirdPhaseEndDay, input.thirdPhaseEndHour, 0, 0, locals.thirdPhaseEndDate); + packNostromoDate(input.listingStartYear, input.listingStartMonth, input.listingStartDay, input.listingStartHour, 0, 0, locals.listingStartDate); + packNostromoDate(input.cliffEndYear, input.cliffEndMonth, input.cliffEndDay, input.cliffEndHour, 0, 0, locals.cliffEndDate); + packNostromoDate(input.vestingEndYear, input.vestingEndMonth, input.vestingEndDay, input.vestingEndHour, 0, 0, locals.vestingEndDate); - // Cancellation is blocked during emergency pause but does not use the scheduled auction timer pause. - if (state.get().isEmergencyPaused) + if (locals.curDate > locals.firstPhaseStartDate || locals.firstPhaseStartDate >= locals.firstPhaseEndDate || locals.firstPhaseEndDate > locals.secondPhaseStartDate || locals.secondPhaseStartDate >= locals.secondPhaseEndDate || locals.secondPhaseEndDate > locals.thirdPhaseStartDate || locals.thirdPhaseStartDate >= locals.thirdPhaseEndDate || locals.thirdPhaseEndDate > locals.listingStartDate || locals.listingStartDate > locals.cliffEndDate || locals.cliffEndDate > locals.vestingEndDate) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = EAuctionError::AuctionPaused; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, - output.cancellationFee); - logProcedureResult(locals.log); - return; + return ; } - - if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + if (checkValidNostDateTime(locals.firstPhaseStartDate) == 0 || checkValidNostDateTime(locals.firstPhaseEndDate) == 0 || checkValidNostDateTime(locals.secondPhaseStartDate) == 0 || checkValidNostDateTime(locals.secondPhaseEndDate) == 0 || checkValidNostDateTime(locals.thirdPhaseStartDate) == 0 || checkValidNostDateTime(locals.thirdPhaseEndDate) == 0 || checkValidNostDateTime(locals.listingStartDate) == 0 || checkValidNostDateTime(locals.cliffEndDate) == 0 || checkValidNostDateTime(locals.vestingEndDate) == 0) { - locals.findAuctionInput.auctionIndex = input.auctionIndex; - CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = locals.findAuctionOutput.found ? EAuctionError::AuctionClosed : EAuctionError::AuctionNotFound; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, - output.cancellationFee); - logProcedureResult(locals.log); - return; + return ; } - if (locals.auction.core.status != EAuctionStatus::Active) + if (input.stepOfVesting == 0 || input.stepOfVesting > 12 || input.TGE > 50 || input.threshold > 50 || input.indexOfProject >= state.get().numberOfCreatedProject) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = EAuctionError::AuctionClosed; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, - output.cancellationFee); - logProcedureResult(locals.log); - return; + return ; } - if (locals.auction.core.seller != qpi.invocator()) + + if (state.get().projects.get(input.indexOfProject).creator != qpi.invocator()) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = EAuctionError::Forbidden; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, - output.cancellationFee); - logProcedureResult(locals.log); - return; + return ; } - if (locals.auction.core.nextBidIndex != 0) + if (input.soldAmount > state.get().projects.get(input.indexOfProject).supplyOfToken) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = EAuctionError::AuctionHasAcceptedBid; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, - output.cancellationFee); - logProcedureResult(locals.log); - return; + return ; } - // The fee base represents the full reserve value of the lot being withdrawn. - locals.cancellationBaseAmount = locals.auction.core.salePrice; - if (locals.auction.core.type == EAuctionType::Batch) - { - locals.cancellationBaseAmount = smul(locals.auction.core.salePrice, locals.auction.core.quantityForSale); - } - output.cancellationFee = calculateBasisPointAmount(locals.cancellationBaseAmount, state.get().auctionCancellationFeeBasisPoints); - - if (static_cast(qpi.invocationReward()) < output.cancellationFee) + if (locals.curDate <= state.get().projects.get(input.indexOfProject).endDate || state.get().projects.get(input.indexOfProject).numberOfYes <= state.get().projects.get(input.indexOfProject).numberOfNo || state.get().projects.get(input.indexOfProject).isCreatedFundarasing == 1) { if (qpi.invocationReward() > 0) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::InsufficientFunds; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, - output.cancellationFee); - logProcedureResult(locals.log); - return; - } - - locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; - locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.core.seller; - CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); - - // Cancellation closes the auction and records it in the same history ring as finalized auctions. - locals.currentDate = qpi.now(); - locals.auction.core.status = EAuctionStatus::Cancelled; - locals.auction.core.settledAt = locals.currentDate; - locals.auction.core.allocatedQuantity = 0; - locals.auction.core.highestBidAmount = 0; - locals.auction.core.highestBidPrice = 0; - locals.auction.core.highestBidQuantity = 0; - locals.auction.core.highestBidder = NULL_ID; - locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; - state.mut().totalCancelledAuctions = sadd(state.get().totalCancelledAuctions, 1ULL); - locals.archiveClosedAuctionInput.auction = locals.auction; - CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); - - // Cancellation fees use the same epoch pool as creation and small-bid service fees. - locals.accumulateAuctionServiceFeeInput.feeAmount = output.cancellationFee; - CALL(AccumulateAuctionServiceFee, locals.accumulateAuctionServiceFeeInput, locals.accumulateAuctionServiceFeeOutput); - - if (static_cast(qpi.invocationReward()) > output.cancellationFee) - { - qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - output.cancellationFee); - } - - output.errorCode = EAuctionError::Success; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, output.cancellationFee); - logProcedureResult(locals.log); - } - - /** - * @brief Lets the seller accept or reject a pending standard auction whose highest bid stayed below the sale price. - * @note The manual decision window lasts one week; after expiry the contract finalizes the sale automatically in favor of the buyer. - */ - PUBLIC_PROCEDURE_WITH_LOCALS(ResolvePendingStandardAuction) - { - output.refundedAmount = 0; - output.errorCode = EAuctionError::InvalidInput; - - // This procedure does not need a reward; return any supplied amount before validation. - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - - CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); - if (locals.isAuctionInteractionPausedOutput.isPaused) - { - output.errorCode = EAuctionError::AuctionPaused; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, - output.refundedAmount); - logProcedureResult(locals.log); - return; - } - - if (input.acceptSale > 1) - { - output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, - output.refundedAmount); - logProcedureResult(locals.log); - return; - } - - if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) - { - locals.findAuctionInput.auctionIndex = input.auctionIndex; - CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); - output.errorCode = locals.findAuctionOutput.found ? EAuctionError::AuctionClosed : EAuctionError::AuctionNotFound; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, - output.refundedAmount); - logProcedureResult(locals.log); - return; - } - - if (locals.auction.core.seller != qpi.invocator()) - { - output.errorCode = EAuctionError::Forbidden; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, - output.refundedAmount); - logProcedureResult(locals.log); - return; - } - - if (locals.auction.core.type != EAuctionType::Standard || locals.auction.core.status != EAuctionStatus::PendingSellerDecision) - { - output.errorCode = EAuctionError::AuctionClosed; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, - output.refundedAmount); - logProcedureResult(locals.log); - return; - } - - // If the decision window has expired, the automatic sale wins over the seller action. - locals.currentDate = qpi.now(); - if (!state.get().isAuctionTimerPaused && locals.auction.core.sellerDecisionDeadline <= locals.currentDate) - { - locals.finalizeStandardAuctionInput.auctionIndex = input.auctionIndex; - locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; - CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); - - output.errorCode = EAuctionError::AuctionClosed; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, - output.refundedAmount); - logProcedureResult(locals.log); - return; - } - - // Accepting finalizes the sale; rejecting refunds the bidder and returns the lot to the seller. - if (input.acceptSale) - { - locals.finalizeStandardAuctionInput.auctionIndex = input.auctionIndex; - locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; - CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); - output.errorCode = locals.finalizeStandardAuctionOutput.success ? EAuctionError::Success : EAuctionError::AuctionClosed; - } - else - { - locals.rejectStandardAuctionInput.auctionIndex = input.auctionIndex; - locals.rejectStandardAuctionInput.currentDate = locals.currentDate; - CALL(RejectStandardAuction, locals.rejectStandardAuctionInput, locals.rejectStandardAuctionOutput); - output.refundedAmount = locals.rejectStandardAuctionOutput.refundedAmount; - output.errorCode = locals.rejectStandardAuctionOutput.success ? EAuctionError::Success : EAuctionError::AuctionClosed; - } - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, - output.refundedAmount); - logProcedureResult(locals.log); - } - - /** - * @brief Overwrites the full auction fee configuration. - * @note Only the configured takeover coordinator can call this procedure. - */ - PUBLIC_PROCEDURE_WITH_LOCALS(SetAuctionFees) - { - output.errorCode = EAuctionError::InvalidInput; - // Administrative procedures never consume invocation rewards. - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - - if (qpi.invocator() != state.get().takeoverCoordinator) - { - output.errorCode = EAuctionError::Forbidden; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFees, output.errorCode, 0, 0); - logProcedureResult(locals.log); - return; - } - - // Validate all fee tiers together so no gross-proceeds tier can exceed 100 percent. - if (!isValidAuctionFeeConfiguration(input.privateAuctionFee, input.publicAuctionCreationFee, input.auctionCancellationFeeBasisPoints, - input.managementFeeBasisPoints, input.developmentFeeBasisPoints, input.takeoverCoordinatorFeeBasisPoints, - input.shareholderDividendBasisPoints, input.shareholderFeeBasisPointsTier1, - input.shareholderFeeBasisPointsTier2, input.shareholderFeeBasisPointsTier3, - input.shareholderFeeBasisPointsTier4)) - { - output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFees, output.errorCode, 0, 0); - logProcedureResult(locals.log); - return; - } - - state.mut().privateAuctionFee = input.privateAuctionFee; - state.mut().publicAuctionCreationFee = input.publicAuctionCreationFee; - state.mut().auctionCancellationFeeBasisPoints = input.auctionCancellationFeeBasisPoints; - state.mut().managementFeeBasisPoints = input.managementFeeBasisPoints; - state.mut().developmentFeeBasisPoints = input.developmentFeeBasisPoints; - state.mut().takeoverCoordinatorFeeBasisPoints = input.takeoverCoordinatorFeeBasisPoints; - state.mut().shareholderDividendBasisPoints = input.shareholderDividendBasisPoints; - state.mut().shareholderFeeBasisPointsTier1 = input.shareholderFeeBasisPointsTier1; - state.mut().shareholderFeeBasisPointsTier2 = input.shareholderFeeBasisPointsTier2; - state.mut().shareholderFeeBasisPointsTier3 = input.shareholderFeeBasisPointsTier3; - state.mut().shareholderFeeBasisPointsTier4 = input.shareholderFeeBasisPointsTier4; - output.errorCode = EAuctionError::Success; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFees, output.errorCode, 0, 0); - logProcedureResult(locals.log); - } - - /** - * @brief Updates every auction fee except the takeover coordinator-specific splits. - * @note Only the configured management wallet can call this procedure. - */ - PUBLIC_PROCEDURE_WITH_LOCALS(SetAuctionFeesByManagement) - { - output.errorCode = EAuctionError::InvalidInput; - - // Management can update operational fees, but takeover-specific fee parameters stay unchanged. - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - - if (qpi.invocator() != state.get().management) - { - output.errorCode = EAuctionError::Forbidden; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFeesByManagement, output.errorCode, 0, 0); - logProcedureResult(locals.log); - return; - } - - if (!isValidAuctionFeeConfiguration(input.privateAuctionFee, input.publicAuctionCreationFee, input.auctionCancellationFeeBasisPoints, - input.managementFeeBasisPoints, input.developmentFeeBasisPoints, - state.get().takeoverCoordinatorFeeBasisPoints, state.get().shareholderDividendBasisPoints, - input.shareholderFeeBasisPointsTier1, input.shareholderFeeBasisPointsTier2, - input.shareholderFeeBasisPointsTier3, input.shareholderFeeBasisPointsTier4)) - { - output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFeesByManagement, output.errorCode, 0, 0); - logProcedureResult(locals.log); - return; - } - - state.mut().privateAuctionFee = input.privateAuctionFee; - state.mut().publicAuctionCreationFee = input.publicAuctionCreationFee; - state.mut().auctionCancellationFeeBasisPoints = input.auctionCancellationFeeBasisPoints; - state.mut().managementFeeBasisPoints = input.managementFeeBasisPoints; - state.mut().developmentFeeBasisPoints = input.developmentFeeBasisPoints; - state.mut().shareholderFeeBasisPointsTier1 = input.shareholderFeeBasisPointsTier1; - state.mut().shareholderFeeBasisPointsTier2 = input.shareholderFeeBasisPointsTier2; - state.mut().shareholderFeeBasisPointsTier3 = input.shareholderFeeBasisPointsTier3; - state.mut().shareholderFeeBasisPointsTier4 = input.shareholderFeeBasisPointsTier4; - output.errorCode = EAuctionError::Success; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFeesByManagement, output.errorCode, 0, 0); - logProcedureResult(locals.log); - } - - /** - * @brief Reassigns the management role to another wallet. - * @note Only the configured takeover coordinator can call this procedure. - */ - PUBLIC_PROCEDURE_WITH_LOCALS(SetManagement) - { - output.errorCode = EAuctionError::InvalidInput; - - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - - if (qpi.invocator() != state.get().takeoverCoordinator) - { - output.errorCode = EAuctionError::Forbidden; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetManagement, output.errorCode, 0, 0); - logProcedureResult(locals.log); - return; - } - - if (isZero(input.management)) - { - output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetManagement, output.errorCode, 0, 0); - logProcedureResult(locals.log); - return; - } - - state.mut().management = input.management; - output.errorCode = EAuctionError::Success; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetManagement, output.errorCode, 0, 0); - logProcedureResult(locals.log); - } - - /** - * @brief Configures the execution fee reserve guard that triggers an emergency pause on a sudden reserve drop. - * @note Only the configured takeover coordinator or management wallet can call this procedure. - */ - PUBLIC_PROCEDURE_WITH_LOCALS(SetFeeReserveGuardConfig) - { - output.errorCode = EAuctionError::InvalidInput; - // Resetting the baseline forces the guard to start a fresh observation window. - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - - if (qpi.invocator() != state.get().takeoverCoordinator && qpi.invocator() != state.get().management) - { - output.errorCode = EAuctionError::Forbidden; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetFeeReserveGuardConfig, output.errorCode, 0, 0); - logProcedureResult(locals.log); - return; - } - - if (input.dropBasisPoints == 0 || input.dropBasisPoints > NOST_BASIS_POINTS_SCALE || input.windowSeconds == 0) - { - output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetFeeReserveGuardConfig, output.errorCode, 0, 0); - logProcedureResult(locals.log); - return; - } - - state.mut().feeReserveGuardDropBasisPoints = input.dropBasisPoints; - state.mut().feeReserveGuardWindowSeconds = input.windowSeconds; - state.mut().feeReserveBaselineAt.setInvalid(); - output.errorCode = EAuctionError::Success; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetFeeReserveGuardConfig, output.errorCode, 0, 0); - logProcedureResult(locals.log); - } - - /** - * @brief Manually pauses or resumes every auction interaction, overriding the automatic execution fee reserve guard. - * @note Only the configured takeover coordinator or management wallet can call this procedure. Resuming clears the guard window so a stale - * baseline cannot immediately retrigger the pause. - */ - PUBLIC_PROCEDURE_WITH_LOCALS(SetEmergencyPause) - { - output.errorCode = EAuctionError::InvalidInput; - // Manual pause shares the same state as the automatic reserve guard. - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - - if (qpi.invocator() != state.get().takeoverCoordinator && qpi.invocator() != state.get().management) - { - output.errorCode = EAuctionError::Forbidden; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetEmergencyPause, output.errorCode, 0, 0); - logProcedureResult(locals.log); - return; - } - - if (input.paused) - { - state.mut().isEmergencyPaused = 1; - state.mut().emergencyPausedAt = qpi.now(); - } - else - { - state.mut().isEmergencyPaused = 0; - state.mut().emergencyPausedAt.setInvalid(); - state.mut().feeReserveBaselineAt.setInvalid(); - } - - output.errorCode = EAuctionError::Success; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetEmergencyPause, output.errorCode, 0, 0); - logProcedureResult(locals.log); - } - - /** - * @brief Returns the stored state of one auction. - * @note The response contains a serializable auction view; access-control containers are returned as fixed arrays with counts. - */ - PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionByIndex) - { - output.found = 0; - locals.findAuctionInput.auctionIndex = input.auctionIndex; - CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); - if (!locals.findAuctionOutput.found) - { - return; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; } - locals.auction = locals.findAuctionOutput.auction; - output.found = 1; - output.auction.core = locals.auction.core; + if (input.tokenPrice * input.soldAmount < input.requiredFunds + div(input.requiredFunds * input.threshold, 100ULL)) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } - // Hash containers are flattened into arrays because they are not part of the public ABI surface. - output.auction.requiredAccessAssetCount = 0; - for (locals.requiredAccessAssetSetIndex = locals.auction.requiredAccessAssets.nextElementIndex(NULL_INDEX); - locals.requiredAccessAssetSetIndex != NULL_INDEX; - locals.requiredAccessAssetSetIndex = locals.auction.requiredAccessAssets.nextElementIndex(locals.requiredAccessAssetSetIndex)) + if (qpi.invocationReward() < NOSTROMO_QX_TOKEN_ISSUANCE_FEE) { - locals.requiredAccessAsset.asset = locals.auction.requiredAccessAssets.key(locals.requiredAccessAssetSetIndex); - locals.requiredAccessAsset.quantity = locals.auction.requiredAccessAssets.value(locals.requiredAccessAssetSetIndex); - output.auction.requiredAccessAssets.set(output.auction.requiredAccessAssetCount, locals.requiredAccessAsset); - output.auction.requiredAccessAssetCount = sadd(output.auction.requiredAccessAssetCount, 1ULL); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; } - output.auction.allowedBidderWalletCount = 0; - for (locals.allowedBidderWalletSetIndex = locals.auction.allowedBidderWallets.nextElementIndex(NULL_INDEX); - locals.allowedBidderWalletSetIndex != NULL_INDEX; - locals.allowedBidderWalletSetIndex = locals.auction.allowedBidderWallets.nextElementIndex(locals.allowedBidderWalletSetIndex)) + if (qpi.invocationReward() > NOSTROMO_QX_TOKEN_ISSUANCE_FEE) { - locals.allowedBidderWallet = locals.auction.allowedBidderWallets.key(locals.allowedBidderWalletSetIndex); - output.auction.allowedBidderWallets.set(output.auction.allowedBidderWalletCount, locals.allowedBidderWallet); - output.auction.allowedBidderWalletCount = sadd(output.auction.allowedBidderWalletCount, 1ULL); + qpi.transfer(qpi.invocator(), qpi.invocationReward() - NOSTROMO_QX_TOKEN_ISSUANCE_FEE); } + + locals.tmpProject = state.get().projects.get(input.indexOfProject); + locals.tmpProject.isCreatedFundarasing = 1; + state.mut().projects.set(input.indexOfProject, locals.tmpProject); + + locals.newFundraising.tokenPrice = input.tokenPrice; + locals.newFundraising.soldAmount = input.soldAmount; + locals.newFundraising.requiredFunds = input.requiredFunds; + locals.newFundraising.raisedFunds = 0; + locals.newFundraising.indexOfProject = input.indexOfProject; + locals.newFundraising.firstPhaseStartDate = locals.firstPhaseStartDate; + locals.newFundraising.firstPhaseEndDate = locals.firstPhaseEndDate; + locals.newFundraising.secondPhaseStartDate = locals.secondPhaseStartDate; + locals.newFundraising.secondPhaseEndDate = locals.secondPhaseEndDate; + locals.newFundraising.thirdPhaseStartDate = locals.thirdPhaseStartDate; + locals.newFundraising.thirdPhaseEndDate = locals.thirdPhaseEndDate; + locals.newFundraising.listingStartDate = locals.listingStartDate; + locals.newFundraising.cliffEndDate = locals.cliffEndDate; + locals.newFundraising.vestingEndDate = locals.vestingEndDate; + locals.newFundraising.threshold = input.threshold; + locals.newFundraising.TGE = input.TGE; + locals.newFundraising.stepOfVesting = input.stepOfVesting; + + state.mut().fundaraisings.set(state.get().numberOfFundraising, locals.newFundraising); + state.mut().numberOfFundraising++; } - /** - * @brief Returns the stored bid state of one wallet in one auction. - * @note The response indicates whether a participant record exists for the requested auction and wallet. - */ - PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionParticipant) + struct investInProject_locals + { + QX::IssueAsset_input input; + QX::IssueAsset_output output; + QX::TransferShareManagementRights_input TransferShareManagementRightsInput; + QX::TransferShareManagementRights_output TransferShareManagementRightsOutput; + investInfo tmpInvestData; + fundaraisingInfo tmpFundraising; + uint64 maxCap, minCap, maxInvestmentPerUser, userInvestedAmount; + uint32 curDate, elementIndex, i, numberOfInvestedProjects; + uint8 tierLevel; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(investInProject) { - output.found = 0; - locals.bestParticipantFound = 0; - // A wallet can have multiple historical batch bid slots; return the newest matching record. - for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) + if (input.indexOfFundraising >= state.get().numberOfFundraising || qpi.invocationReward() == 0) { - locals.participantData = state.get().participants.get(locals.participantSlotIndex); - if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex || - locals.participantData.participant != input.participant) + if (qpi.invocationReward() > 0) { - continue; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + return ; + } - if (!locals.bestParticipantFound || locals.participantData.bidIndex > output.participantData.bidIndex) + locals.maxCap = state.get().fundaraisings.get(input.indexOfFundraising).requiredFunds + div(state.get().fundaraisings.get(input.indexOfFundraising).requiredFunds * state.get().fundaraisings.get(input.indexOfFundraising).threshold, 100ULL); + locals.minCap = state.get().fundaraisings.get(input.indexOfFundraising).requiredFunds - div(state.get().fundaraisings.get(input.indexOfFundraising).requiredFunds * state.get().fundaraisings.get(input.indexOfFundraising).threshold, 100ULL); + if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects) && locals.numberOfInvestedProjects >= NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST) + { + if (qpi.invocationReward() > 0) { - locals.bestParticipantFound = 1; - locals.bestParticipantSlotIndex = locals.participantSlotIndex; - output.participantData = locals.participantData; - output.found = 1; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + return ; } - // Search archived slots as well because displaced and settled bids are removed from the live array. - for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participantHistory.capacity(); ++locals.participantSlotIndex) + + packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); + + locals.tmpFundraising = state.get().fundaraisings.get(input.indexOfFundraising); + + if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).firstPhaseStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).firstPhaseEndDate) { - locals.participantData = state.get().participantHistory.get(locals.participantSlotIndex); - if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex || - locals.participantData.participant != input.participant) + if (state.get().users.contains(qpi.invocator()) == 0) { - continue; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; } - if (!locals.bestParticipantFound || locals.participantData.bidIndex > output.participantData.bidIndex) + + state.get().users.get(qpi.invocator(), locals.tierLevel); + switch (locals.tierLevel) { - locals.bestParticipantFound = 1; - output.participantData = locals.participantData; - output.found = 1; + case 1: + locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT, state.get().totalPoolWeight); + break; + case 2: + locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT, state.get().totalPoolWeight); + break; + case 3: + locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_DOG_POOL_WEIGHT, state.get().totalPoolWeight); + break; + case 4: + locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT, state.get().totalPoolWeight); + break; + case 5: + locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_WARRIOR_POOL_WEIGHT, state.get().totalPoolWeight); + break; + default: + break; } - } - } - - /** - * @brief Returns the remaining post-BEGIN_EPOCH pause before auction interactions resume. - * @note This getter exposes the 500-tick launch pause referenced by the auction timing rules. - */ - PUBLIC_FUNCTION_WITH_LOCALS(GetTicksBeforeAuctionLaunch) - { - output.ticks = 0; - if (!state.get().isPostBeginEpochPauseArmed) - { - return; - } + state.get().investors.get(qpi.invocator(), state.mut().tmpInvestedList); + state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects); - output.ticks = static_cast(max(static_cast(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) - - (static_cast(qpi.tick()) - static_cast(qpi.initialTick())), - 0)); - } + for (locals.i = 0; locals.i < locals.numberOfInvestedProjects; locals.i++) + { + if (state.get().tmpInvestedList.get(locals.i).indexOfFundraising == input.indexOfFundraising) + { + locals.userInvestedAmount = state.get().tmpInvestedList.get(locals.i).investedAmount; + break; + } + } - /** - * @brief Returns the current auction fee configuration stored in contract state. - * @note The response includes creation, cancellation, revenue split, and tier-based shareholder fee parameters. - */ - PUBLIC_FUNCTION(GetAuctionFees) - { - output.privateAuctionFee = state.get().privateAuctionFee; - output.auctionCancellationFeeBasisPoints = state.get().auctionCancellationFeeBasisPoints; - output.managementFeeBasisPoints = state.get().managementFeeBasisPoints; - output.developmentFeeBasisPoints = state.get().developmentFeeBasisPoints; - output.takeoverCoordinatorFeeBasisPoints = state.get().takeoverCoordinatorFeeBasisPoints; - output.shareholderDividendBasisPoints = state.get().shareholderDividendBasisPoints; - output.shareholderFeeBasisPointsTier1 = state.get().shareholderFeeBasisPointsTier1; - output.shareholderFeeBasisPointsTier2 = state.get().shareholderFeeBasisPointsTier2; - output.shareholderFeeBasisPointsTier3 = state.get().shareholderFeeBasisPointsTier3; - output.shareholderFeeBasisPointsTier4 = state.get().shareholderFeeBasisPointsTier4; - output.publicAuctionCreationFee = state.get().publicAuctionCreationFee; - } + locals.tmpInvestData.indexOfFundraising = input.indexOfFundraising; - /** - * @brief Calculates the escrow, accumulated fee, and reward required by Batch Auction bid arithmetic. - * @param input Prospective bid quantity and price per asset; zero values are accepted for arithmetic inspection. - * @param output Saturating escrow product, small-bid fee, and saturating total reward. - * @note Non-zero escrow pays enough fee to reach `NOST_BATCH_BID_FEE_CUTOFF`; escrow at or above the cutoff pays no bid fee. - * @note This function does not validate whether `PlaceBid` would accept the bid or mutate contract state. - */ - PUBLIC_FUNCTION(CalculateBatchAuctionBidFee) { calculateBatchAuctionBidFee(input.bidQuantity, input.bidAmount, output); } + if (locals.i < locals.numberOfInvestedProjects) + { + if (locals.tmpFundraising.raisedFunds + locals.maxInvestmentPerUser - locals.userInvestedAmount > locals.maxCap) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } + if (qpi.invocationReward() + locals.userInvestedAmount > locals.maxInvestmentPerUser) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() + locals.userInvestedAmount - locals.maxInvestmentPerUser); - /** - * @brief Returns the current wallets that receive auction fee transfers. - * @note The response exposes the configured management, development, and takeover coordinator addresses. - */ - PUBLIC_FUNCTION(GetFeeRecipients) - { - output.management = state.get().management; - output.development = state.get().development; - output.takeoverCoordinator = state.get().takeoverCoordinator; - } + locals.tmpInvestData.investedAmount = locals.maxInvestmentPerUser; + locals.tmpFundraising.raisedFunds += locals.maxInvestmentPerUser - locals.userInvestedAmount; + } + else + { + locals.tmpInvestData.investedAmount = qpi.invocationReward() + locals.userInvestedAmount; + locals.tmpFundraising.raisedFunds += qpi.invocationReward(); + } + state.mut().tmpInvestedList.set(locals.i, locals.tmpInvestData); + state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); + } + else + { + if (locals.tmpFundraising.raisedFunds + locals.maxInvestmentPerUser > locals.maxCap) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } + if (qpi.invocationReward() > (sint64)locals.maxInvestmentPerUser) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.maxInvestmentPerUser); + locals.tmpInvestData.investedAmount = locals.maxInvestmentPerUser; + locals.tmpFundraising.raisedFunds += locals.maxInvestmentPerUser; + } + else + { + locals.tmpInvestData.investedAmount = qpi.invocationReward(); + locals.tmpFundraising.raisedFunds += qpi.invocationReward(); + } - /** - * @brief Returns the ring buffer with recently closed auctions. - * @note The buffer stores auction identifiers for both finalized and cancelled auctions. - * @note When `totalEntries` exceeds `NOST_AUCTION_HISTORY_NUM`, older entries are overwritten in ring-buffer order. - */ - PUBLIC_FUNCTION_WITH_LOCALS(GetClosedAuctionHistory) - { - // Preserve physical ring positions to keep the existing auctionIndices ABI stable for clients. - for (locals.historyIndex = 0; locals.historyIndex < state.get().closedAuctionHistory.capacity(); ++locals.historyIndex) + state.mut().tmpInvestedList.set(locals.numberOfInvestedProjects, locals.tmpInvestData); + state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); + if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects)) + { + state.mut().numberOfInvestedProjects.set(qpi.invocator(), locals.numberOfInvestedProjects + 1); + } + else + { + state.mut().numberOfInvestedProjects.set(qpi.invocator(), 1); + } + } + } + else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).secondPhaseStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).secondPhaseEndDate) { - locals.auction = state.get().closedAuctionHistory.get(locals.historyIndex); - if (locals.auction.core.status != EAuctionStatus::None) + if (state.get().users.contains(qpi.invocator()) == 0) { - output.auctionIndices.set(locals.historyIndex, locals.auction.core.auctionIndex); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; } - } - output.totalEntries = state.get().closedAuctionHistoryCounter; - } - /** - * @brief Returns whether the temporary fee override routes every fee to development. - */ - PUBLIC_FUNCTION(GetRouteAllFeesToDevelopment) { output.enabled = state.get().routeAllFeesToDevelopment; } + state.get().users.get(qpi.invocator(), locals.tierLevel); + if (locals.tierLevel < 4) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } + switch (locals.tierLevel) + { + case 4: + locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT, state.get().totalPoolWeight); + break; + case 5: + locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_WARRIOR_POOL_WEIGHT, state.get().totalPoolWeight); + break; + default: + break; + } - /** - * @brief Returns the aggregate shared fee amount awaiting `END_EPOCH` settlement. - * @note The legacy field name is retained for ABI compatibility. - */ - PUBLIC_FUNCTION(GetPendingServiceFeePool) { output.pendingServiceFeePool = getNostromoFeePoolTotal(state.get().feePool); } + state.get().investors.get(qpi.invocator(), state.mut().tmpInvestedList); + state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects); - /** @brief Returns every accumulator in the shared Nostromo fee pool. */ - PUBLIC_FUNCTION(GetNostromoFeePool) - { - output.feePool = state.get().feePool; - output.totalAmount = getNostromoFeePoolTotal(state.get().feePool); - } + for (locals.i = 0; locals.i < locals.numberOfInvestedProjects; locals.i++) + { + if (state.get().tmpInvestedList.get(locals.i).indexOfFundraising == input.indexOfFundraising) + { + locals.userInvestedAmount = state.get().tmpInvestedList.get(locals.i).investedAmount; + break; + } + } - /** @brief Returns the QU obligation currently registered for one wallet. */ - PUBLIC_FUNCTION(GetPendingPayout) - { - output.amount = 0; - state.get().pendingQuPayouts.get(input.account, output.amount); - } + locals.tmpInvestData.indexOfFundraising = input.indexOfFundraising; - /** - * @brief Returns the current state of the execution fee reserve guard, including a live reserve reading. - */ - PUBLIC_FUNCTION(GetFeeReserveGuardState) - { - output.currentFeeReserve = qpi.queryFeeReserve(SELF_INDEX); - output.feeReserveBaseline = state.get().feeReserveBaseline; - output.feeReserveBaselineAt = state.get().feeReserveBaselineAt; - output.emergencyPausedAt = state.get().emergencyPausedAt; - output.dropBasisPoints = state.get().feeReserveGuardDropBasisPoints; - output.windowSeconds = state.get().feeReserveGuardWindowSeconds; - output.isEmergencyPaused = state.get().isEmergencyPaused; - } + if (locals.i < locals.numberOfInvestedProjects) + { + if (locals.tmpFundraising.raisedFunds + locals.maxInvestmentPerUser - locals.userInvestedAmount > locals.maxCap) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } + if (qpi.invocationReward() + locals.userInvestedAmount > locals.maxInvestmentPerUser) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() + locals.userInvestedAmount - locals.maxInvestmentPerUser); - /** - * @brief Returns aggregate auction, participant, fee, and pause counters. - */ - PUBLIC_FUNCTION_WITH_LOCALS(GetContractStats) - { - output.stats.totalAuctionsCreated = state.get().totalAuctionsCreated; - output.stats.closedAuctionHistoryCounter = state.get().closedAuctionHistoryCounter; - output.stats.auctionShareholderDividendPool = state.get().auctionShareholderDividendPool; - output.stats.pendingServiceFeePool = getNostromoFeePoolTotal(state.get().feePool); - output.stats.totalPendingQuPayouts = state.get().totalPendingQuPayouts; - output.stats.retainedClosedAuctionCount = min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); - output.stats.retainedParticipantHistoryCount = min(state.get().participantHistoryCounter, state.get().participantHistory.capacity()); - output.stats.finalizedAuctionCount = state.get().totalFinalizedAuctions; - output.stats.cancelledAuctionCount = state.get().totalCancelledAuctions; - output.stats.qxTransferFee = state.get().qxTransferFee; - output.stats.routeAllFeesToDevelopment = state.get().routeAllFeesToDevelopment; - output.stats.isAuctionTimerPaused = state.get().isAuctionTimerPaused; - output.stats.isPostBeginEpochPauseArmed = state.get().isPostBeginEpochPauseArmed; - output.stats.isEmergencyPaused = state.get().isEmergencyPaused; - - // Stats scan fixed storage because participant slots and auction records are not separately indexed by status. - for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) - { - locals.participantData = state.get().participants.get(locals.participantSlotIndex); - if (locals.participantData.isUsed) + locals.tmpInvestData.investedAmount = locals.maxInvestmentPerUser; + locals.tmpFundraising.raisedFunds += locals.maxInvestmentPerUser - locals.userInvestedAmount; + } + else + { + locals.tmpInvestData.investedAmount = qpi.invocationReward() + locals.userInvestedAmount; + locals.tmpFundraising.raisedFunds += qpi.invocationReward(); + } + state.mut().tmpInvestedList.set(locals.i, locals.tmpInvestData); + state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); + } + else { - output.stats.participantCount = sadd(output.stats.participantCount, 1ULL); + if (locals.tmpFundraising.raisedFunds + locals.maxInvestmentPerUser > locals.maxCap) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } + if (qpi.invocationReward() > (sint64)locals.maxInvestmentPerUser) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.maxInvestmentPerUser); + locals.tmpInvestData.investedAmount = locals.maxInvestmentPerUser; + locals.tmpFundraising.raisedFunds += locals.maxInvestmentPerUser; + } + else + { + locals.tmpInvestData.investedAmount = qpi.invocationReward(); + locals.tmpFundraising.raisedFunds += qpi.invocationReward(); + } + + state.mut().tmpInvestedList.set(locals.numberOfInvestedProjects, locals.tmpInvestData); + state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); + if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects)) + { + state.mut().numberOfInvestedProjects.set(qpi.invocator(), locals.numberOfInvestedProjects + 1); + } + else + { + state.mut().numberOfInvestedProjects.set(qpi.invocator(), 1); + } } } - - for (locals.auctionElementIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); locals.auctionElementIndex != NULL_INDEX; - locals.auctionElementIndex = state.get().auctionList.nextElementIndex(locals.auctionElementIndex)) + else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).thirdPhaseStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).thirdPhaseEndDate) { - locals.auction = state.get().auctionList.value(locals.auctionElementIndex); - switch (locals.auction.core.status) + if (locals.tmpFundraising.raisedFunds + qpi.invocationReward() > locals.maxCap) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } + state.get().investors.get(qpi.invocator(), state.mut().tmpInvestedList); + state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects); + + for (locals.i = 0; locals.i < locals.numberOfInvestedProjects; locals.i++) { - case EAuctionStatus::Active: output.stats.activeAuctionCount = sadd(output.stats.activeAuctionCount, 1ULL); break; - case EAuctionStatus::PendingSellerDecision: - output.stats.pendingSellerDecisionAuctionCount = sadd(output.stats.pendingSellerDecisionAuctionCount, 1ULL); + if (state.get().tmpInvestedList.get(locals.i).indexOfFundraising == input.indexOfFundraising) + { + locals.userInvestedAmount = state.get().tmpInvestedList.get(locals.i).investedAmount; break; - default: break; + } } - } - } - /** - * @brief Returns a page of auction summaries ordered by creation index. - */ - PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionSummaries) - { - // Live and archived records are disjoint, so the retained total does not require an ordered scan. - output.totalCount = - sadd(state.get().auctionList.population(), min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity())); - output.returnedCount = 0; - locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); - if (locals.boundedLimit == 0 || input.offset >= output.totalCount) - { - return; + locals.tmpInvestData.indexOfFundraising = input.indexOfFundraising; + + if (locals.i < locals.numberOfInvestedProjects) + { + locals.tmpInvestData.investedAmount = qpi.invocationReward() + locals.userInvestedAmount; + state.mut().tmpInvestedList.set(locals.i, locals.tmpInvestData); + state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); + } + else + { + locals.tmpInvestData.investedAmount = qpi.invocationReward(); + + state.mut().tmpInvestedList.set(locals.numberOfInvestedProjects, locals.tmpInvestData); + state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); + if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects)) + { + state.mut().numberOfInvestedProjects.set(qpi.invocator(), locals.numberOfInvestedProjects + 1); + } + else + { + state.mut().numberOfInvestedProjects.set(qpi.invocator(), 1); + } + } + locals.tmpFundraising.raisedFunds += qpi.invocationReward(); } - locals.scannedAuctionCount = 0; - locals.selectNextAuctionInput.hasAfterAuctionIndex = 0; - locals.selectNextAuctionInput.includeClosedAuctions = 1; - locals.selectNextAuctionInput.filterBySeller = 0; - // Cursor selection reconstructs creation order across unordered live storage and the closed-history ring. - while (output.returnedCount < locals.boundedLimit && locals.scannedAuctionCount < output.totalCount) + else { - CALL(SelectNextRetainedAuction, locals.selectNextAuctionInput, locals.selectNextAuctionOutput); - if (!locals.selectNextAuctionOutput.found) + if (qpi.invocationReward() > 0) { - break; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - locals.auction = locals.selectNextAuctionOutput.auction; - // Skip only the requested prefix; the exact total is already available without scanning the remainder. - if (locals.scannedAuctionCount >= input.offset) + return ; + } + if (locals.minCap <= locals.tmpFundraising.raisedFunds && locals.tmpFundraising.isCreatedToken == 0) + { + locals.input.assetName = state.get().projects.get(locals.tmpFundraising.indexOfProject).tokenName; + locals.input.numberOfDecimalPlaces = 0; + locals.input.numberOfShares = state.get().projects.get(locals.tmpFundraising.indexOfProject).supplyOfToken; + locals.input.unitOfMeasurement = 0; + + INVOKE_OTHER_CONTRACT_PROCEDURE(QX, IssueAsset, locals.input, locals.output, NOSTROMO_QX_TOKEN_ISSUANCE_FEE); + + if (locals.output.issuedNumberOfShares == state.get().projects.get(locals.tmpFundraising.indexOfProject).supplyOfToken) { - fillAuctionSummary(locals.auction, locals.auctionSummary); - output.auctions.set(output.returnedCount, locals.auctionSummary); - output.returnedCount = sadd(output.returnedCount, 1ULL); + locals.tmpFundraising.isCreatedToken = 1; + + locals.TransferShareManagementRightsInput.asset.assetName = state.get().projects.get(locals.tmpFundraising.indexOfProject).tokenName; + locals.TransferShareManagementRightsInput.asset.issuer = SELF; + locals.TransferShareManagementRightsInput.newManagingContractIndex = SELF_INDEX; + locals.TransferShareManagementRightsInput.numberOfShares = state.get().projects.get(locals.tmpFundraising.indexOfProject).supplyOfToken; + + INVOKE_OTHER_CONTRACT_PROCEDURE(QX, TransferShareManagementRights, locals.TransferShareManagementRightsInput, locals.TransferShareManagementRightsOutput, 0); + + qpi.transferShareOwnershipAndPossession(state.get().projects.get(locals.tmpFundraising.indexOfProject).tokenName, SELF, SELF, SELF, state.get().projects.get(locals.tmpFundraising.indexOfProject).supplyOfToken - locals.tmpFundraising.soldAmount, state.get().projects.get(locals.tmpFundraising.indexOfProject).creator); } - locals.scannedAuctionCount = sadd(locals.scannedAuctionCount, 1ULL); - locals.selectNextAuctionInput.afterAuctionIndex = locals.auction.core.auctionIndex; - locals.selectNextAuctionInput.hasAfterAuctionIndex = 1; } + + state.mut().fundaraisings.set(input.indexOfFundraising, locals.tmpFundraising); + } - /** - * @brief Returns a page of active or pending-seller-decision auction indices. - */ - PUBLIC_FUNCTION_WITH_LOCALS(GetActiveAuctionIndices) + struct claimToken_locals { - // Invariant: terminal auctions are archived and removed, so every live-map entry is active or awaiting a seller decision. - output.totalCount = state.get().auctionList.population(); - output.returnedCount = 0; - locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); - if (locals.boundedLimit == 0 || input.offset >= output.totalCount) + investInfo tmpInvestData; + uint64 maxClaimAmount, investedAmount, dayA, dayB, start_cur_diffSecond, cur_end_diffSecond, claimedAmount; + uint32 curDate, tmpDate, numberOfInvestedProjects; + sint32 i, j; + uint8 curVestingStep, vestingPercent; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(claimToken) + { + packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); + + if (input.indexOfFundraising >= state.get().numberOfFundraising) { - return; + return ; + } + + state.get().investors.get(qpi.invocator(), state.mut().tmpInvestedList); + if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects) == 0) + { + return ; } - locals.scannedAuctionCount = 0; - locals.selectNextAuctionInput.hasAfterAuctionIndex = 0; - locals.selectNextAuctionInput.includeClosedAuctions = 0; - locals.selectNextAuctionInput.filterBySeller = 0; - // Select only the requested live-map prefix and page; closed history cannot contain active auctions. - while (output.returnedCount < locals.boundedLimit && locals.scannedAuctionCount < output.totalCount) + for (locals.i = 0; locals.i < (sint32)locals.numberOfInvestedProjects; locals.i++) { - CALL(SelectNextRetainedAuction, locals.selectNextAuctionInput, locals.selectNextAuctionOutput); - if (!locals.selectNextAuctionOutput.found) + if (state.get().tmpInvestedList.get(locals.i).indexOfFundraising == input.indexOfFundraising) { + locals.investedAmount = state.get().tmpInvestedList.get(locals.i).investedAmount; + locals.claimedAmount = state.get().tmpInvestedList.get(locals.i).claimedAmount; + locals.tmpInvestData = state.get().tmpInvestedList.get(locals.i); break; } - locals.selectNextAuctionInput.afterAuctionIndex = locals.selectNextAuctionOutput.auction.core.auctionIndex; - locals.selectNextAuctionInput.hasAfterAuctionIndex = 1; - if (locals.scannedAuctionCount >= input.offset) - { - output.auctionIndices.set(output.returnedCount, locals.selectNextAuctionOutput.auction.core.auctionIndex); - output.returnedCount = sadd(output.returnedCount, 1ULL); - } - locals.scannedAuctionCount = sadd(locals.scannedAuctionCount, 1ULL); } - } - /** - * @brief Returns a page of auction summaries created by a seller. - */ - PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionsBySeller) - { - output.returnedCount = 0; - locals.countAuctionsInput.seller = input.seller; - CALL(CountRetainedAuctionsBySeller, locals.countAuctionsInput, locals.countAuctionsOutput); - output.totalCount = locals.countAuctionsOutput.count; - locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); - if (locals.boundedLimit == 0 || input.offset >= output.totalCount) + if (locals.i == locals.numberOfInvestedProjects) { - return; + return ; + } + + if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).listingStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate) + { + locals.maxClaimAmount = div(div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice) * state.get().fundaraisings.get(input.indexOfFundraising).TGE, 100ULL); + } + else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate) + { + locals.tmpDate = state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate; + diffDateInSecond(locals.tmpDate, locals.curDate, locals.j, locals.dayA, locals.dayB, locals.start_cur_diffSecond); + locals.tmpDate = state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate; + diffDateInSecond(locals.curDate, locals.tmpDate, locals.j, locals.dayA, locals.dayB, locals.cur_end_diffSecond); + + locals.curVestingStep = (uint8)div(locals.start_cur_diffSecond, div(locals.start_cur_diffSecond + locals.cur_end_diffSecond, state.get().fundaraisings.get(input.indexOfFundraising).stepOfVesting * 1ULL)) + 1; + locals.vestingPercent = (uint8)div(100ULL - state.get().fundaraisings.get(input.indexOfFundraising).TGE, state.get().fundaraisings.get(input.indexOfFundraising).stepOfVesting * 1ULL) * locals.curVestingStep; + locals.maxClaimAmount = div(div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice) * (state.get().fundaraisings.get(input.indexOfFundraising).TGE + locals.vestingPercent), 100ULL); + } + else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate) + { + locals.maxClaimAmount = div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice); } - locals.scannedAuctionCount = 0; - locals.selectNextAuctionInput.seller = input.seller; - locals.selectNextAuctionInput.hasAfterAuctionIndex = 0; - locals.selectNextAuctionInput.includeClosedAuctions = 1; - locals.selectNextAuctionInput.filterBySeller = 1; - // The selector skips other sellers, so only the requested seller's prefix and page are ordered. - while (output.returnedCount < locals.boundedLimit && locals.scannedAuctionCount < output.totalCount) + if (input.amount + locals.claimedAmount > locals.maxClaimAmount) + { + return ; + } + else { - CALL(SelectNextRetainedAuction, locals.selectNextAuctionInput, locals.selectNextAuctionOutput); - if (!locals.selectNextAuctionOutput.found) + qpi.transferShareOwnershipAndPossession(state.get().projects.get(state.get().fundaraisings.get(input.indexOfFundraising).indexOfProject).tokenName, SELF, SELF, SELF, input.amount, qpi.invocator()); + if (input.amount + locals.claimedAmount == locals.maxClaimAmount && state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate <= locals.curDate) { - break; + state.mut().tmpInvestedList.set(locals.i, state.get().tmpInvestedList.get(locals.numberOfInvestedProjects - 1)); + state.mut().numberOfInvestedProjects.set(qpi.invocator(), locals.numberOfInvestedProjects - 1); + } + else + { + locals.tmpInvestData.claimedAmount = input.amount + locals.claimedAmount; + state.mut().tmpInvestedList.set(locals.i, locals.tmpInvestData); } - locals.auction = locals.selectNextAuctionOutput.auction; - locals.selectNextAuctionInput.afterAuctionIndex = locals.auction.core.auctionIndex; - locals.selectNextAuctionInput.hasAfterAuctionIndex = 1; - if (locals.scannedAuctionCount >= input.offset) + state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); + state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects); + if (locals.numberOfInvestedProjects == 0) { - fillAuctionSummary(locals.auction, locals.auctionSummary); - output.auctions.set(output.returnedCount, locals.auctionSummary); - output.returnedCount = sadd(output.returnedCount, 1ULL); + state.mut().investors.removeByKey(qpi.invocator()); + state.mut().numberOfInvestedProjects.removeByKey(qpi.invocator()); } - locals.scannedAuctionCount = sadd(locals.scannedAuctionCount, 1ULL); + output.claimedAmount = input.amount; } } - /** - * @brief Looks up the first auction matching a metadata CID. - */ - PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionByMetadataCid) + struct upgradeTier_locals { - output.found = 0; - output.auctionIndex = 0; - locals.findAuctionInput.metadataIpfsCid = input.metadataIpfsCid; - CALL(FindFirstRetainedAuctionByMetadataCid, locals.findAuctionInput, locals.findAuctionOutput); - // The helper compares all retained candidates and returns the smallest matching creation index. - if (!locals.findAuctionOutput.found) - { - return; - } - output.found = 1; - output.auctionIndex = locals.findAuctionOutput.auction.core.auctionIndex; - fillAuctionSummary(locals.findAuctionOutput.auction, output.auction); - } - - /** - * @brief Returns auction summaries for a batch of requested indices. - */ - PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionSummariesByIndexBatch) + uint64 deltaAmount; + uint32 i, deltaPoolWeight; + uint8 currentTierLevel; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(upgradeTier) { - output.returnedCount = 0; - locals.boundedLimit = min(input.count, NOST_AUCTION_GETTER_PAGE_SIZE); - // Preserve input positions so callers can correlate each requested index with its found flag. - for (locals.requestedIndex = 0; locals.requestedIndex < locals.boundedLimit; ++locals.requestedIndex) + if (state.get().users.contains(qpi.invocator()) == 0) { - locals.auctionIndex = input.auctionIndices.get(locals.requestedIndex); - locals.findAuctionInput.auctionIndex = locals.auctionIndex; - CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); - if (locals.findAuctionOutput.found) + if (qpi.invocationReward() > 0) { - locals.auction = locals.findAuctionOutput.auction; - fillAuctionSummary(locals.auction, locals.auctionSummary); - output.auctions.set(locals.requestedIndex, locals.auctionSummary); - output.found.set(locals.requestedIndex, 1); - output.returnedCount = sadd(output.returnedCount, 1ULL); + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + return ; } - } - /** - * @brief Returns a page of participants for one auction. - */ - PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionParticipants) - { - output.totalCount = 0; - output.returnedCount = 0; - locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); - // Participant storage is global, so auction participant pages are built by scanning all slots. - for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) + state.get().users.get(qpi.invocator(), locals.currentTierLevel); + + switch (locals.currentTierLevel) { - locals.participantData = state.get().participants.get(locals.participantSlotIndex); - if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) - { - continue; - } - if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) - { - fillParticipantSummary(locals.participantData, locals.participantSummary); - output.participants.set(output.returnedCount, locals.participantSummary); - output.returnedCount = sadd(output.returnedCount, 1ULL); - } - output.totalCount = sadd(output.totalCount, 1ULL); + case 1: + locals.deltaAmount = NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT - NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT; + locals.deltaPoolWeight = NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT - NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT; + break; + case 2: + locals.deltaAmount = NOSTROMO_TIER_DOG_STAKE_AMOUNT - NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT; + locals.deltaPoolWeight = NOSTROMO_TIER_DOG_POOL_WEIGHT - NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; + break; + case 3: + locals.deltaAmount = NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT - NOSTROMO_TIER_DOG_STAKE_AMOUNT; + locals.deltaPoolWeight = NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT - NOSTROMO_TIER_DOG_POOL_WEIGHT; + break; + case 4: + locals.deltaAmount = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT - NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT; + locals.deltaPoolWeight = NOSTROMO_TIER_WARRIOR_POOL_WEIGHT - NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; + break; + default: + break; } - // Append archived records after live records so an offset spans both storage tiers deterministically. - for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participantHistory.capacity(); ++locals.participantSlotIndex) + if (input.newTierLevel != locals.currentTierLevel + 1 || qpi.invocationReward() < (sint64)locals.deltaAmount) { - locals.participantData = state.get().participantHistory.get(locals.participantSlotIndex); - if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) + if (qpi.invocationReward() > 0) { - continue; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) + return ; + } + else + { + state.mut().users.set(qpi.invocator(), input.newTierLevel); + if (qpi.invocationReward() > (sint64)locals.deltaAmount) { - fillParticipantSummary(locals.participantData, locals.participantSummary); - output.participants.set(output.returnedCount, locals.participantSummary); - output.returnedCount = sadd(output.returnedCount, 1ULL); + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.deltaAmount); } - output.totalCount = sadd(output.totalCount, 1ULL); + state.mut().totalPoolWeight += locals.deltaPoolWeight; } } - /** - * @brief Returns a page of historical auction participations for one wallet. - */ - PUBLIC_FUNCTION_WITH_LOCALS(GetUserParticipations) + PUBLIC_PROCEDURE(TransferShareManagementRights) { - output.totalCount = 0; - output.returnedCount = 0; - locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); - // User participation history includes inactive records so settled and displaced bids remain visible. - for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) + if (qpi.invocationReward() < state.get().transferRightsFee) { - locals.participantData = state.get().participants.get(locals.participantSlotIndex); - if (!locals.participantData.isUsed || locals.participantData.participant != input.participant) - { - continue; - } - if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) + return ; + } + + if (qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer,qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.numberOfShares) + { + // not enough shares available + output.transferredNumberOfShares = 0; + if (qpi.invocationReward() > 0) { - fillUserParticipationSummary(locals.participantData.auctionIndex, locals.participantData, locals.userParticipationSummary); - output.participations.set(output.returnedCount, locals.userParticipationSummary); - output.returnedCount = sadd(output.returnedCount, 1ULL); + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.totalCount = sadd(output.totalCount, 1ULL); } - // Continue the same page over archived bids after accounting for matching live entries. - for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participantHistory.capacity(); ++locals.participantSlotIndex) + else { - locals.participantData = state.get().participantHistory.get(locals.participantSlotIndex); - if (!locals.participantData.isUsed || locals.participantData.participant != input.participant) + if (qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, + input.newManagingContractIndex, input.newManagingContractIndex, state.get().transferRightsFee) < 0) { - continue; + // error + output.transferredNumberOfShares = 0; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } } - if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) + else { - fillUserParticipationSummary(locals.participantData.auctionIndex, locals.participantData, locals.userParticipationSummary); - output.participations.set(output.returnedCount, locals.userParticipationSummary); - output.returnedCount = sadd(output.returnedCount, 1ULL); + // success + output.transferredNumberOfShares = input.numberOfShares; + if (qpi.invocationReward() > state.get().transferRightsFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.get().transferRightsFee); + } } - output.totalCount = sadd(output.totalCount, 1ULL); } } - /** - * @brief Returns the most recently created auction index when one exists. - */ - PUBLIC_FUNCTION(GetLatestAuctionIndex) + PUBLIC_FUNCTION(getStats) { - output.found = state.get().totalAuctionsCreated > 0; - output.auctionIndex = output.found ? state.get().totalAuctionsCreated - 1 : 0; + output.epochRevenue = state.get().epochRevenue; + output.numberOfCreatedProject = state.get().numberOfCreatedProject; + output.numberOfFundraising = state.get().numberOfFundraising; + output.numberOfRegister = state.get().numberOfRegister; + output.totalPoolWeight = state.get().totalPoolWeight; } - /** - * @brief Counts auctions created by a seller. - */ - PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionCountBySeller) + PUBLIC_FUNCTION(getTierLevelByUser) { - locals.countAuctionsInput.seller = input.seller; - CALL(CountRetainedAuctionsBySeller, locals.countAuctionsInput, locals.countAuctionsOutput); - output.count = locals.countAuctionsOutput.count; + state.get().users.get(input.userId, output.tierLevel); } - /** - * @brief Returns immutable creation-time fields for an auction. - */ - PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionAtCreationSnapshot) + PUBLIC_FUNCTION(getUserVoteStatus) { - output.found = 0; - locals.findAuctionInput.auctionIndex = input.auctionIndex; - CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); - if (!locals.findAuctionOutput.found) - { - return; - } - locals.auction = locals.findAuctionOutput.auction; - output.found = 1; - output.seller = locals.auction.core.seller; - output.createdAt = locals.auction.core.createdAt; - output.auctionIndex = locals.auction.core.auctionIndex; - output.quantityForSale = locals.auction.core.quantityForSale; - output.initialPrice = locals.auction.core.initialPrice; - output.salePrice = locals.auction.core.salePrice; - output.minimumBidIncrement = locals.auction.core.minimumBidIncrement; - output.buyNowPrice = locals.auction.core.buyNowPrice; - output.auctionDurationSeconds = locals.auction.core.auctionDurationSeconds; - output.type = static_cast(locals.auction.core.type); - output.visibility = static_cast(locals.auction.core.visibility); + state.get().numberOfVotedProject.get(input.userId, output.numberOfVotedProjects); + state.get().voteStatus.get(input.userId, output.projectIndexList); } - /** - * @brief Returns current read-only guidance for the next valid Batch Auction bid. - * @note `found` also covers closed auctions while their snapshots remain in retained history. - * @note `PlaceBid` re-runs the same availability validation before accepting a bid. - */ - PUBLIC_FUNCTION_WITH_LOCALS(GetBatchAuctionBidAvailability) + PUBLIC_FUNCTION(checkTokenCreatability) { - locals.computeBatchBidAvailabilityInput.auctionIndex = input.auctionIndex; - locals.computeBatchBidAvailabilityInput.bidAmount = 0; - CALL(ComputeBatchBidAvailability, locals.computeBatchBidAvailabilityInput, output); - // Live auctions are fully classified by the availability helper, including non-Batch auctions. - if (output.found) - { - return; - } - - // A retained closed auction still exists for lookup purposes, but can never accept another bid. - locals.isClosedAuctionRetainedInput.auctionIndex = input.auctionIndex; - CALL(IsClosedAuctionRetained, locals.isClosedAuctionRetainedInput, locals.isClosedAuctionRetainedOutput); - output.found = locals.isClosedAuctionRetainedOutput.found; + output.result = state.get().tokens.contains(input.tokenName); } - /** - * @brief Transfers share management rights for an asset position to another managing contract. - * @note The caller must currently possess at least the requested number of shares. - * @note The caller must send the destination contract's required transfer fee as invocation reward. This contract cannot query that - * fee before calling `releaseShares`, so callers must resolve it from `newManagingContractIndex`. - */ - PUBLIC_PROCEDURE_WITH_LOCALS(TransferShareManagementRights) + PUBLIC_FUNCTION(getNumberOfInvestedProjects) { - locals.reward = qpi.invocationReward(); - locals.refundAmount = locals.reward; - locals.success = false; - output.transferredNumberOfShares = 0; - output.errorCode = EAuctionError::InvalidInput; - - // Emergency pause blocks cross-contract share release and returns the caller's fee budget. - if (state.get().isEmergencyPaused) - { - if (locals.refundAmount > 0) - { - qpi.transfer(qpi.invocator(), locals.refundAmount); - } - output.errorCode = EAuctionError::AuctionPaused; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::TransferShareManagementRights, output.errorCode, 0, - output.transferredNumberOfShares); - logProcedureResult(locals.log); - return; - } - - // `releaseShares` consumes only the destination transfer fee; any unused reward is refunded below. - if (input.numberOfShares > 0 && qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer, qpi.invocator(), qpi.invocator(), - SELF_INDEX, SELF_INDEX) >= input.numberOfShares) - { - locals.result = qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, input.newManagingContractIndex, - input.newManagingContractIndex, locals.reward); - if (locals.result != INVALID_AMOUNT && locals.result >= 0) - { - locals.success = true; - locals.refundAmount = locals.reward - locals.result; - } - } - - if (locals.success) - { - output.transferredNumberOfShares = input.numberOfShares; - output.errorCode = EAuctionError::Success; - } - - if (locals.refundAmount > 0) - { - qpi.transfer(qpi.invocator(), locals.refundAmount); - } - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::TransferShareManagementRights, output.errorCode, 0, - output.transferredNumberOfShares); - - logProcedureResult(locals.log); + state.get().numberOfInvestedProjects.get(input.userId, output.numberOfInvestedProjects); } -protected: - /** - * @brief Emits a procedure log as success or error based on its error code. - */ - static void logProcedureResult(const NostromoProcedureLog& log) +public: + struct getProjectByIndex_input { - if (log.errorCode == static_cast(EAuctionError::Success)) - { - LOG_INFO(log); - } - else - { - LOG_ERROR(log); - } - } + uint32 indexOfProject; + }; - /** - * @brief Fills the common procedure log payload. - */ - static void setProcedureLogInput(NostromoProcedureLog& log, const id& actor, EProcedureId procedure, EAuctionError errorCode, uint64 auctionIndex, - sint64 amount) + struct getProjectByIndex_output { - log.contractIndex = SELF_INDEX; - log.procedure = static_cast(procedure); - log.errorCode = static_cast(errorCode); - log.auctionIndex = auctionIndex; - log.actor = actor; - log.amount = amount; - log._terminator = 0; - } + projectInfo project; + }; - /** - * @brief Copies persisted auction data into a compact summary. - */ - static void fillAuctionSummary(const AuctionData& auction, AuctionSummary& summary) + PUBLIC_FUNCTION(getProjectByIndex) { - summary.metadataIpfsCid = auction.core.metadataIpfsCid; - summary.seller = auction.core.seller; - summary.highestBidder = auction.core.highestBidder; - summary.createdAt = auction.core.createdAt; - summary.settledAt = auction.core.settledAt; - summary.auctionIndex = auction.core.auctionIndex; - summary.quantityForSale = auction.core.quantityForSale; - summary.allocatedQuantity = auction.core.allocatedQuantity; - summary.initialPrice = auction.core.initialPrice; - summary.salePrice = auction.core.salePrice; - summary.buyNowPrice = auction.core.buyNowPrice; - summary.highestBidPrice = auction.core.highestBidPrice; - summary.highestBidQuantity = auction.core.highestBidQuantity; - summary.highestBidAmount = auction.core.highestBidAmount; - summary.type = static_cast(auction.core.type); - summary.visibility = static_cast(auction.core.visibility); - summary.status = static_cast(auction.core.status); + output.project = state.get().projects.get(input.indexOfProject); } - /** - * @brief Copies participant storage data into an auction participant summary. - */ - static void fillParticipantSummary(const AuctionParticipantData& participantData, ParticipantSummary& summary) + struct getFundarasingByIndex_input { - summary.participant = participantData.participant; - summary.lastBidTime = participantData.lastBidTime; - summary.bidAmount = participantData.bidAmount; - summary.escrowedAmount = participantData.escrowedAmount; - summary.requestedQuantity = participantData.requestedQuantity; - summary.allocatedQuantity = participantData.allocatedQuantity; - summary.isWinningBid = participantData.isWinningBid; - } + uint32 indexOfFundarasing; + }; - /** - * @brief Copies participant storage data into a user participation summary. - */ - static void fillUserParticipationSummary(uint64 auctionIndex, const AuctionParticipantData& participantData, UserParticipationSummary& summary) + struct getFundarasingByIndex_output { - summary.participant = participantData.participant; - summary.lastBidTime = participantData.lastBidTime; - summary.auctionIndex = auctionIndex; - summary.bidAmount = participantData.bidAmount; - summary.escrowedAmount = participantData.escrowedAmount; - summary.requestedQuantity = participantData.requestedQuantity; - summary.allocatedQuantity = participantData.allocatedQuantity; - summary.isWinningBid = participantData.isWinningBid; - } + fundaraisingInfo fundarasing; + }; - /** - * @brief Returns the smaller of two values. - */ - template - static constexpr T min(const T& a, const T& b) + PUBLIC_FUNCTION(getFundarasingByIndex) { - return (a < b) ? a : b; + output.fundarasing = state.get().fundaraisings.get(input.indexOfFundarasing); } - /** - * @brief Returns the larger of two values. - */ - template - static constexpr T max(const T& a, const T& b) + + struct getProjectIndexListByCreator_input { - return a > b ? a : b; - } + id creator; + }; - /** - * @brief Resolves Batch Auction quantity invariants from creation input. - */ - static bool resolveBatchAuctionCreateParams(uint64 lotItemCount, uint64 totalEscrowQuantity, uint64 minimumPurchaseQuantity, - uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPrice) + struct getProjectIndexListByCreator_output { - quantityForSale = 0; - resolvedMinimumPurchaseQuantity = 0; - if (lotItemCount != NOST_BATCH_AUCTION_LOT_ITEM_NUM || totalEscrowQuantity == 0 || minimumPurchaseQuantity == 0 || - minimumPurchaseQuantity > totalEscrowQuantity || buyNowPrice != 0) - { - return false; - } - quantityForSale = totalEscrowQuantity; - resolvedMinimumPurchaseQuantity = minimumPurchaseQuantity; - return true; - } + Array indexListForProjects; + }; - /** - * @brief Resolves Standard Auction quantity and price invariants from creation input. - */ - static bool resolveStandardAuctionCreateParams(uint64 minimumBidIncrement, uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity, - uint64 buyNowPrice, uint64 initialPrice, uint64 salePrice) + struct getProjectIndexListByCreator_locals + { + uint32 i, countOfProject; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getProjectIndexListByCreator) { - quantityForSale = 0; - resolvedMinimumPurchaseQuantity = 0; - if (initialPrice < NOST_STANDARD_MIN_PRICE || salePrice < NOST_STANDARD_MIN_PRICE || minimumBidIncrement < NOST_STANDARD_MIN_BID_INCREMENT) + for (locals.i = 0; locals.i < state.get().numberOfCreatedProject; locals.i++) { - return false; + if (state.get().projects.get(locals.i).creator == input.creator) + { + output.indexListForProjects.set(locals.countOfProject++, locals.i); + } } - - if (initialPrice > salePrice) + for (locals.i = locals.countOfProject; locals.i < NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST; locals.i++) { - return false; + output.indexListForProjects.set(locals.i, NOSTROMO_MAX_NUMBER_PROJECT); } + } - if (buyNowPrice > 0 && (buyNowPrice < initialPrice || buyNowPrice < salePrice)) - { + struct getInfoUserInvested_input + { + id investorId; + }; - return false; - } + struct getInfoUserInvested_output + { + Array listUserInvested; + }; - quantityForSale = NOST_STANDARD_AUCTION_LOT_COUNT; - resolvedMinimumPurchaseQuantity = 0; - return true; - } + struct getInfoUserInvested_locals + { + uint32 i, countOfProject; + }; - /** - * @brief Validates that private auctions use at least one supported access mode. - */ - constexpr static bool validatePrivateAuctionAccess(EAuctionVisibility visibility, uint64 requiredAccessAssetCount, uint64 allowedWalletCount) + PUBLIC_FUNCTION_WITH_LOCALS(getInfoUserInvested) { - return visibility != EAuctionVisibility::Private || requiredAccessAssetCount > 0 || allowedWalletCount > 0; + state.get().investors.get(input.investorId, output.listUserInvested); } - /** - * @brief Validates governance fee percentages and fixed service fees. - */ - constexpr static bool isValidAuctionFeeConfiguration(sint64 privateAuctionFee, sint64 publicAuctionCreationFee, - uint64 auctionCancellationFeeBasisPoints, uint64 managementFeeBasisPoints, - uint64 developmentFeeBasisPoints, uint64 takeoverCoordinatorFeeBasisPoints, - uint64 shareholderDividendBasisPoints, uint64 shareholderFeeBasisPointsTier1, - uint64 shareholderFeeBasisPointsTier2, uint64 shareholderFeeBasisPointsTier3, - uint64 shareholderFeeBasisPointsTier4) + struct getMaxClaimAmount_input { - return privateAuctionFee >= 0 && publicAuctionCreationFee >= 0 && auctionCancellationFeeBasisPoints <= NOST_BASIS_POINTS_SCALE && - managementFeeBasisPoints <= NOST_BASIS_POINTS_SCALE && developmentFeeBasisPoints <= NOST_BASIS_POINTS_SCALE && - takeoverCoordinatorFeeBasisPoints <= NOST_BASIS_POINTS_SCALE && shareholderDividendBasisPoints <= NOST_BASIS_POINTS_SCALE && - shareholderFeeBasisPointsTier1 <= NOST_BASIS_POINTS_SCALE && shareholderFeeBasisPointsTier2 <= NOST_BASIS_POINTS_SCALE && - shareholderFeeBasisPointsTier3 <= NOST_BASIS_POINTS_SCALE && shareholderFeeBasisPointsTier4 <= NOST_BASIS_POINTS_SCALE && - (shareholderFeeBasisPointsTier1 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= - NOST_BASIS_POINTS_SCALE && - (shareholderFeeBasisPointsTier2 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= - NOST_BASIS_POINTS_SCALE && - (shareholderFeeBasisPointsTier3 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= - NOST_BASIS_POINTS_SCALE && - (shareholderFeeBasisPointsTier4 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= - NOST_BASIS_POINTS_SCALE; - } + id investorId; + uint32 indexOfFundraising; + }; - /** - * @brief Selects the shareholder fee tier for a gross auction amount. - */ - static uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount, const StateData& state) + struct getMaxClaimAmount_output + { + uint64 amount; + }; + + struct getMaxClaimAmount_locals + { + Array tmpInvestedList; + investInfo tmpInvestData; + uint64 maxClaimAmount, investedAmount, dayA, dayB, dayC, dayD, start_cur_diffSecond, cur_end_diffSecond, claimedAmount; + uint32 curDate, tmpDate, numberOfInvestedProjects; + sint32 i, j, k; + uint8 curVestingStep, vestingPercent; + bit flag; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getMaxClaimAmount) { - if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1) + packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); + + if (input.indexOfFundraising >= state.get().numberOfFundraising) { - return state.shareholderFeeBasisPointsTier1; + return ; } - if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2) + + state.get().investors.get(input.investorId, locals.tmpInvestedList); + if (state.get().numberOfInvestedProjects.get(input.investorId, locals.numberOfInvestedProjects) == 0) { - return state.shareholderFeeBasisPointsTier2; + return ; } - if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3) + + for (locals.i = 0; locals.i < (sint32)locals.numberOfInvestedProjects; locals.i++) { - return state.shareholderFeeBasisPointsTier3; + if (locals.tmpInvestedList.get(locals.i).indexOfFundraising == input.indexOfFundraising) + { + locals.investedAmount = locals.tmpInvestedList.get(locals.i).investedAmount; + locals.claimedAmount = locals.tmpInvestedList.get(locals.i).claimedAmount; + locals.tmpInvestData = locals.tmpInvestedList.get(locals.i); + break; + } } - return state.shareholderFeeBasisPointsTier4; - } - /** - * @brief Selects the shareholder fee tier from contract state. - */ - static uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount, const ContractState& state) - { - return getAuctionShareholderFeeBasisPoints(grossAmount, state.get()); - } + if (locals.i == locals.numberOfInvestedProjects) + { + return ; + } - /** @brief Returns the zero-based shareholder fee tier selected by an auction gross amount. */ - constexpr static uint64 getAuctionShareholderFeeTierIndex(uint64 grossAmount) - { - if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1) + if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).listingStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate) { - return 0; + output.amount = div(div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice) * state.get().fundaraisings.get(input.indexOfFundraising).TGE, 100ULL); } - if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2) + else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate) { - return 1; + locals.tmpDate = state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate; + diffDateInSecond(locals.tmpDate, locals.curDate, locals.j, locals.dayA, locals.dayB, locals.start_cur_diffSecond); + locals.tmpDate = state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate; + diffDateInSecond(locals.curDate, locals.tmpDate, locals.k, locals.dayC, locals.dayD, locals.cur_end_diffSecond); + + locals.curVestingStep = (uint8)div(locals.start_cur_diffSecond, div(locals.start_cur_diffSecond + locals.cur_end_diffSecond, state.get().fundaraisings.get(input.indexOfFundraising).stepOfVesting * 1ULL)) + 1; + locals.vestingPercent = (uint8)div(100ULL - state.get().fundaraisings.get(input.indexOfFundraising).TGE, state.get().fundaraisings.get(input.indexOfFundraising).stepOfVesting * 1ULL) * locals.curVestingStep; + output.amount = div(div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice) * (state.get().fundaraisings.get(input.indexOfFundraising).TGE + locals.vestingPercent), 100ULL); } - if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3) + else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate) { - return 2; + output.amount = div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice); } - return 3; } - /** @brief Returns the saturating aggregate of every unsettled fee-pool accumulator. */ - static uint64 getNostromoFeePoolTotal(const NostromoFeePool& feePool) + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() { - return sadd(sadd(sadd(feePool.shareholderDividendTier1Amount, feePool.shareholderDividendTier2Amount), - sadd(feePool.shareholderDividendTier3Amount, feePool.shareholderDividendTier4Amount)), - sadd(sadd(feePool.commonServiceFeeAmount, feePool.shareholderDividendAmount), - sadd(sadd(feePool.managementAmount, feePool.developmentAmount), feePool.takeoverCoordinatorAmount))); - } + REGISTER_USER_FUNCTION(getStats, 1); + REGISTER_USER_FUNCTION(getTierLevelByUser, 2); + REGISTER_USER_FUNCTION(getUserVoteStatus, 3); + REGISTER_USER_FUNCTION(checkTokenCreatability, 4); + REGISTER_USER_FUNCTION(getNumberOfInvestedProjects, 5); + REGISTER_USER_FUNCTION(getProjectByIndex, 6); + REGISTER_USER_FUNCTION(getFundarasingByIndex, 7); + REGISTER_USER_FUNCTION(getProjectIndexListByCreator, 8); + REGISTER_USER_FUNCTION(getInfoUserInvested, 9); + REGISTER_USER_FUNCTION(getMaxClaimAmount, 10); - /** - * @brief Computes `floor(amount * basisPoints / 10000)` without overflowing the intermediate product. - */ - static uint64 calculateBasisPointAmount(uint64 amount, uint64 basisPoints) - { - return sadd(smul(div(amount, NOST_BASIS_POINTS_SCALE), basisPoints), - div(smul(mod(amount, NOST_BASIS_POINTS_SCALE), basisPoints), NOST_BASIS_POINTS_SCALE)); + REGISTER_USER_PROCEDURE(registerInTier, 1); + REGISTER_USER_PROCEDURE(logoutFromTier, 2); + REGISTER_USER_PROCEDURE(createProject, 3); + REGISTER_USER_PROCEDURE(voteInProject, 4); + REGISTER_USER_PROCEDURE(createFundraising, 5); + REGISTER_USER_PROCEDURE(investInProject, 6); + REGISTER_USER_PROCEDURE(claimToken, 7); + REGISTER_USER_PROCEDURE(upgradeTier, 8); + REGISTER_USER_PROCEDURE(TransferShareManagementRights, 9); } - /** - * @brief Computes the exact auction fee split without performing transfers. - * @note Keep this helper pure so tests can reuse the same arithmetic as `DistributeAuctionRevenue`. - */ - static void calculateAuctionRevenueBreakdown(uint64 grossAmount, const ContractState& state, - AuctionRevenueBreakdown& output) + INITIALIZE() { - output.sellerPayout = grossAmount; - output.shareholderFeeBasisPoints = getAuctionShareholderFeeBasisPoints(grossAmount, state); - output.shareholderFeeAmount = calculateBasisPointAmount(grossAmount, output.shareholderFeeBasisPoints); - output.shareholderDividendAmount = calculateBasisPointAmount(output.shareholderFeeAmount, state.get().shareholderDividendBasisPoints); - output.managementFeeAmount = calculateBasisPointAmount(grossAmount, state.get().managementFeeBasisPoints); - output.developmentFeeAmount = calculateBasisPointAmount(grossAmount, state.get().developmentFeeBasisPoints); - output.takeoverCoordinatorBaseAmount = calculateBasisPointAmount(grossAmount, state.get().takeoverCoordinatorFeeBasisPoints); - output.takeoverCoordinatorFeeAmount = output.takeoverCoordinatorBaseAmount + (output.shareholderFeeAmount - output.shareholderDividendAmount); - output.sellerPayout = grossAmount - output.shareholderFeeAmount - output.managementFeeAmount - output.developmentFeeAmount - - output.takeoverCoordinatorBaseAmount; + state.mut().teamAddress = ID(_G, _E, _H, _N, _R, _F, _U, _O, _I, _I, _C, _S, _B, _C, _S, _R, _F, _M, _N, _J, _T, _C, _J, _K, _C, _J, _H, _A, _T, _Z, _X, _A, _X, _Y, _O, _F, _W, _X, _U, _F, _L, _C, _K, _F, _P, _B, _W, _X, _Q, _A, _C, _B, _S, _Z, _F, _F); + state.mut().transferRightsFee = 100; } - /** - * @brief Computes the exact service-fee split without performing transfers. - * @note Keep this helper pure so tests can reuse the same arithmetic as `DistributeNostromoFeePool`. - */ - static void calculateAuctionServiceFeeBreakdown(uint64 feeAmount, AuctionServiceFeeBreakdown& output) + struct END_EPOCH_locals { - output.shareholderDividendAmount = calculateBasisPointAmount(feeAmount, NOST_AUCTION_SERVICE_FEE_SHAREHOLDER_BP); - output.managementFeeAmount = calculateBasisPointAmount(feeAmount, NOST_AUCTION_SERVICE_FEE_MANAGEMENT_BP); - output.developmentFeeAmount = calculateBasisPointAmount(feeAmount, NOST_AUCTION_SERVICE_FEE_DEVELOPMENT_BP); - output.takeoverCoordinatorFeeAmount = calculateBasisPointAmount(feeAmount, NOST_AUCTION_SERVICE_FEE_TAKEOVER_COORDINATOR_BP); - // Shareholders receive the rounding remainder so the entire collected fee is distributed on-chain. - output.shareholderDividendAmount = - sadd(output.shareholderDividendAmount, feeAmount - output.shareholderDividendAmount - output.managementFeeAmount - - output.developmentFeeAmount - output.takeoverCoordinatorFeeAmount); - } + fundaraisingInfo tmpFundraising; + investInfo tmpInvest; + Array votedList; + Array clearedVotedList; + id userId; + sint64 idx; + uint32 numberOfVotedProject, clearedNumberOfVotedProject, i, j, curDate, indexOfProject, numberOfInvestedProjects, tierLevel; + }; - /** - * @brief Computes escrow, bid fee, and required reward for a Batch Auction bid. - */ - static void calculateBatchAuctionBidFee(uint64 bidQuantity, uint64 bidAmount, CalculateBatchAuctionBidFee_output& output) + END_EPOCH_WITH_LOCALS() { - output.escrowAmount = smul(bidQuantity, bidAmount); - if (output.escrowAmount == 0) - { - output.fee = 0; - output.requiredReward = 0; - return; - } - - output.fee = output.escrowAmount <= NOST_BATCH_BID_FEE_CUTOFF ? NOST_BATCH_BID_FEE_CUTOFF - output.escrowAmount : 0; - output.requiredReward = sadd(output.escrowAmount, output.fee); - } + packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); - /** - * @brief Returns the service fee required to create an auction. - */ - static sint64 getCreateAuctionFee(EAuctionVisibility visibility, const ContractState& state) - { - switch (visibility) + locals.idx = state.get().investors.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) { - case EAuctionVisibility::Public: return state.get().publicAuctionCreationFee; break; - case EAuctionVisibility::Private: return state.get().privateAuctionFee; break; - default: break; + locals.userId = state.get().investors.key(locals.idx); + state.get().investors.get(locals.userId, state.mut().tmpInvestedList); + state.get().numberOfInvestedProjects.get(locals.userId, locals.numberOfInvestedProjects); + + for (locals.i = 0; locals.i < locals.numberOfInvestedProjects; locals.i++) + { + if (state.get().fundaraisings.get(locals.i).thirdPhaseEndDate < locals.curDate && state.get().fundaraisings.get(locals.i).isCreatedToken == 0 && state.get().fundaraisings.get(locals.i).raisedFunds != 0) + { + qpi.transfer(locals.userId, state.get().tmpInvestedList.get(locals.i).investedAmount); + state.mut().tmpInvestedList.set(locals.i, state.get().tmpInvestedList.get(--locals.numberOfInvestedProjects)); + } + } + if (locals.numberOfInvestedProjects == 0) + { + state.mut().investors.removeByKey(locals.userId); + state.mut().numberOfInvestedProjects.removeByKey(locals.userId); + } + else + { + state.mut().investors.set(locals.userId, state.get().tmpInvestedList); + state.mut().numberOfInvestedProjects.set(locals.userId, locals.numberOfInvestedProjects); + } + locals.idx = state.get().investors.nextElementIndex(locals.idx); } - return 0; - } - /** - * @brief Returns whether an auction type is accepted by the contract. - */ - static bool isSupportedAuctionType(EAuctionType auctionType) - { - return auctionType == EAuctionType::Batch || auctionType == EAuctionType::Standard; - } + for (locals.i = 0; locals.i < state.get().numberOfFundraising; locals.i++) + { + if (state.get().fundaraisings.get(locals.i).thirdPhaseEndDate < locals.curDate && state.get().fundaraisings.get(locals.i).isCreatedToken == 0 && state.get().fundaraisings.get(locals.i).raisedFunds != 0) + { + locals.tmpFundraising = state.get().fundaraisings.get(locals.i); + locals.tmpFundraising.raisedFunds = 0; + state.mut().fundaraisings.set(locals.i, locals.tmpFundraising); + } + else if (state.get().fundaraisings.get(locals.i).thirdPhaseEndDate < locals.curDate && state.get().fundaraisings.get(locals.i).isCreatedToken == 1 && state.get().fundaraisings.get(locals.i).raisedFunds != 0) + { + locals.tmpFundraising = state.get().fundaraisings.get(locals.i); - /** - * @brief Returns whether an auction visibility is accepted by the contract. - */ - static bool isSupportedAuctionVisibility(EAuctionVisibility visibility) - { - return visibility == EAuctionVisibility::Public || visibility == EAuctionVisibility::Private; - } + state.mut().epochRevenue += div(locals.tmpFundraising.raisedFunds * 5, 100ULL); + qpi.transfer(state.get().projects.get(locals.tmpFundraising.indexOfProject).creator, locals.tmpFundraising.raisedFunds - div(locals.tmpFundraising.raisedFunds * 5, 100ULL)); - /** - * @brief Returns whether an asset entry is empty. - */ - static bool isZeroAsset(const Asset& asset) { return asset.assetName == 0 && isZero(asset.issuer); } + qpi.transferShareOwnershipAndPossession(state.get().projects.get(locals.tmpFundraising.indexOfProject).tokenName, SELF, SELF, SELF, state.get().fundaraisings.get(locals.i).soldAmount - div(locals.tmpFundraising.raisedFunds, state.get().fundaraisings.get(locals.i).tokenPrice), state.get().projects.get(locals.tmpFundraising.indexOfProject).creator); - /** @brief Returns whether the runtime fee override routes every auction fee to the development wallet. */ - static bool routeAllFeesToDevelopment(const QPI::ContractState& state) - { - return state.get().routeAllFeesToDevelopment; - } + locals.tmpFundraising.raisedFunds = 0; + state.mut().fundaraisings.set(locals.i, locals.tmpFundraising); + } + } - /** - * @brief Packs year, month, and day into the contract date-stamp format. - */ - static void makeDateStamp(uint8 year, uint8 month, uint8 day, uint32& res) - { - res = static_cast(year << NOST_DATE_STAMP_YEAR_SHIFT | month << NOST_DATE_STAMP_MONTH_SHIFT | day); - } + qpi.transfer(state.get().teamAddress, div(state.get().epochRevenue, 10ULL)); + state.mut().epochRevenue -= div(state.get().epochRevenue, 10ULL); + qpi.distributeDividends(div(state.get().epochRevenue, 676ULL)); + state.mut().epochRevenue -= div(state.get().epochRevenue, 676ULL) * 676; - /** - * @brief Expands an accumulated pause window to include a candidate window. - */ - static void accumulatePauseWindow(uint8& hasPauseWindow, DateAndTime& pauseStartedAt, DateAndTime& pauseEndsAt, - const DateAndTime& candidatePauseStartedAt, const DateAndTime& candidatePauseEndsAt) - { - if (!hasPauseWindow) + locals.idx = state.get().users.nextElementIndex(NULL_INDEX); + while (locals.idx != NULL_INDEX) { - hasPauseWindow = 1; - pauseStartedAt = candidatePauseStartedAt; - pauseEndsAt = candidatePauseEndsAt; - return; - } + locals.userId = state.get().users.key(locals.idx); + locals.tierLevel = state.get().users.value(locals.idx); - if (candidatePauseStartedAt < pauseStartedAt) - { - pauseStartedAt = candidatePauseStartedAt; - } - if (candidatePauseEndsAt > pauseEndsAt) - { - pauseEndsAt = candidatePauseEndsAt; - } - } + if (state.get().numberOfVotedProject.get(locals.userId, locals.numberOfVotedProject)) + { + state.get().voteStatus.get(locals.userId, locals.votedList); + locals.clearedNumberOfVotedProject = 0; + for (locals.j = 0; locals.j < locals.numberOfVotedProject; locals.j++) + { + locals.indexOfProject = locals.votedList.get(locals.j); - /** - * @brief Compares two Nostromo timestamps. - * @param a Left-hand date-time. - * @param b Right-hand date-time. - * @return `-1` if `a < b`, `0` if `a == b`, `1` if `a > b`. - */ - static sint32 dateCompare(const DateAndTime& a, const DateAndTime& b) - { - if (a < b) - { - return -1; - } - if (a > b) - { - return 1; - } - return 0; - } + if (state.get().projects.get(locals.indexOfProject).endDate > locals.curDate) + { + locals.clearedVotedList.set(locals.clearedNumberOfVotedProject++, locals.indexOfProject); + } + } + if (locals.clearedNumberOfVotedProject == 0) + { + state.mut().numberOfVotedProject.removeByKey(locals.userId); + state.mut().voteStatus.removeByKey(locals.userId); + } + else + { + state.mut().numberOfVotedProject.set(locals.userId, locals.clearedNumberOfVotedProject); + state.mut().voteStatus.set(locals.userId, locals.clearedVotedList); + } + } - /** - * @brief Computes the difference in seconds between two `DateAndTime` values. - * @param a Start date-time. - * @param b End date-time. - * @param res Output difference in seconds, or `0` when `A >= B`. - */ - static void diffDateInSecond(const DateAndTime& a, const DateAndTime& b, uint64& res) - { - if (a >= b) - { - res = 0; - return; + locals.idx = state.get().users.nextElementIndex(locals.idx); } - res = div(a.durationMicrosec(b), NOST_MICROSECONDS_PER_SECOND); + + if (state.get().users.needsCleanup()) { state.mut().users.cleanup(); } + if (state.get().investors.needsCleanup()) { state.mut().investors.cleanup(); } + if (state.get().numberOfInvestedProjects.needsCleanup()) { state.mut().numberOfInvestedProjects.cleanup(); } + if (state.get().numberOfVotedProject.needsCleanup()) { state.mut().numberOfVotedProject.cleanup(); } + if (state.get().voteStatus.needsCleanup()) { state.mut().voteStatus.cleanup(); } } + + PRE_ACQUIRE_SHARES() + { + output.allowTransfer = true; + } }; diff --git a/src/qpi/impl/qpi_system_impl.h b/src/qpi/impl/qpi_system_impl.h index bb98845d..383dacad 100644 --- a/src/qpi/impl/qpi_system_impl.h +++ b/src/qpi/impl/qpi_system_impl.h @@ -1,19 +1,14 @@ -#pragma once - -#include "qpi/qpi.h" -#include "system.h" - -unsigned short QPI::QpiContextFunctionCall::epoch() const -{ - return system.epoch; -} - -unsigned int QPI::QpiContextFunctionCall::tick() const -{ - return system.tick; -} - -unsigned int QPI::QpiContextFunctionCall::initialTick() const -{ - return system.initialTick; -} +#pragma once + +#include "qpi/qpi.h" +#include "system.h" + +unsigned short QPI::QpiContextFunctionCall::epoch() const +{ + return system.epoch; +} + +unsigned int QPI::QpiContextFunctionCall::tick() const +{ + return system.tick; +} diff --git a/src/qpi/qpi_context.h b/src/qpi/qpi_context.h index 032e79af..07854a4d 100644 --- a/src/qpi/qpi_context.h +++ b/src/qpi/qpi_context.h @@ -185,9 +185,6 @@ namespace QPI inline uint32 tick( ) const; // [0..999'999'999] - inline uint32 initialTick( - ) const; - inline uint8 year( ) const; // [0..99] (0 = 2000, 1 = 2001, ..., 99 = 2099) diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index a16d2233..6f9c8e67 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -1,4011 +1,1692 @@ #define NO_UEFI -#include "contract_testing.h" - -using namespace QPI; - -namespace -{ - static constexpr uint64 QX_ISSUE_ASSET_FEE = 1000000000ULL; - static constexpr uint64 QX_TRANSFER_ASSET_FEE = 1000000ULL; - static const id NOST_CONTRACT_ID(NOST_CONTRACT_INDEX, 0, 0, 0); -} // namespace - -class ContractTestingNOST : protected ContractTesting -{ -public: - ContractTestingNOST() - { - initEmptySpectrum(); - initEmptyUniverse(); - INIT_CONTRACT(NOST); - system.initialTick = system.tick; - system.epoch = contractDescriptions[NOST_CONTRACT_INDEX].constructionEpoch + 10; - callSystemProcedure(NOST_CONTRACT_INDEX, INITIALIZE); - INIT_CONTRACT(QX); - callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); - setNow(2026, 1, 1, 9, 0, 0); - callSystemProcedure(NOST_CONTRACT_INDEX, END_TICK); - } - - void ensureUser(const id& user, sint64 amount = 1000) - { - if (getBalance(user) == 0) - { - increaseEnergy(user, amount); - } - } - - void seedUser(const id& user, sint64 amount = 2000000000LL) { increaseEnergy(user, amount); } - - void setNow(uint16 year, uint8 month, uint8 day, uint8 hour, uint8 minute, uint8 second) - { - utcTime.Year = year; - utcTime.Month = month; - utcTime.Day = day; - utcTime.Hour = hour; - utcTime.Minute = minute; - utcTime.Second = second; - utcTime.Nanosecond = 0; - updateQpiTime(); - } - - void advanceAndEndTick(uint64 milliseconds) - { - advanceTimeAndTick(milliseconds); - callSystemProcedure(NOST_CONTRACT_INDEX, END_TICK); - } - - void advanceTicks(uint32 count, uint64 millisecondsPerTick = 1000ULL) - { - for (uint32 i = 0; i < count; ++i) - { - advanceAndEndTick(millisecondsPerTick); - } - } - - void beginEpoch() - { - system.initialTick = system.tick; - ++system.epoch; - callSystemProcedure(NOST_CONTRACT_INDEX, BEGIN_EPOCH); - } - - void endEpoch() { callSystemProcedure(NOST_CONTRACT_INDEX, END_EPOCH); } - - sint64 issueAsset(const id& issuer, uint64 assetName, sint64 numberOfShares) - { - QX::IssueAsset_input input{}; - QX::IssueAsset_output output{}; - - input.assetName = assetName; - input.numberOfShares = numberOfShares; - input.unitOfMeasurement = 0; - input.numberOfDecimalPlaces = 0; - - seedUser(issuer, QX_ISSUE_ASSET_FEE); - invokeUserProcedure(QX_CONTRACT_INDEX, 1, input, output, issuer, QX_ISSUE_ASSET_FEE); - return output.issuedNumberOfShares; - } - - sint64 transferAsset(const id& owner, const id& recipient, const Asset& asset, sint64 numberOfShares) - { - QX::TransferShareOwnershipAndPossession_input input{}; - QX::TransferShareOwnershipAndPossession_output output{}; - - input.issuer = asset.issuer; - input.newOwnerAndPossessor = recipient; - input.assetName = asset.assetName; - input.numberOfShares = numberOfShares; - - seedUser(owner, QX_TRANSFER_ASSET_FEE); - invokeUserProcedure(QX_CONTRACT_INDEX, 2, input, output, owner, QX_TRANSFER_ASSET_FEE); - return output.transferredNumberOfShares; - } - - sint64 transferShareManagementRightsToNostromo(const id& owner, const Asset& asset, sint64 numberOfShares) - { - QX::TransferShareManagementRights_input input{}; - QX::TransferShareManagementRights_output output{}; - - input.asset = asset; - input.numberOfShares = numberOfShares; - input.newManagingContractIndex = NOST_CONTRACT_INDEX; - - invokeUserProcedure(QX_CONTRACT_INDEX, 9, input, output, owner, 0); - return output.transferredNumberOfShares; - } - - NOST::CreateAuction_output createAuction(const id& seller, const NOST::CreateAuction_input& input, - sint64 reward = NOST_PUBLIC_AUCTION_CREATION_FEE) - { - if (reward > 0) - { - seedUser(seller, reward); - } - else - { - ensureUser(seller); - } - return createAuctionWithFundedReward(seller, input, reward); - } - - NOST::CreateAuction_output createAuctionWithFundedReward(const id& seller, const NOST::CreateAuction_input& input, sint64 reward) - { - NOST::CreateAuction_output output{}; - invokeUserProcedure(NOST_CONTRACT_INDEX, 1, input, output, seller, reward); - return output; - } - - NOST::PlaceBid_output placeBid(const id& bidder, uint64 auctionIndex, uint64 quantity, uint64 bidAmount, sint64 reward) - { - NOST::PlaceBid_input input{}; - NOST::PlaceBid_output output{}; - - input.auctionIndex = auctionIndex; - input.quantity = quantity; - input.bidAmount = bidAmount; - - seedUser(bidder, reward); - invokeUserProcedure(NOST_CONTRACT_INDEX, 2, input, output, bidder, reward); - return output; - } - - NOST::PlaceBid_output placeBidWithFundedReward(const id& bidder, uint64 auctionIndex, uint64 quantity, uint64 bidAmount, sint64 reward) - { - NOST::PlaceBid_input input{}; - NOST::PlaceBid_output output{}; - - input.auctionIndex = auctionIndex; - input.quantity = quantity; - input.bidAmount = bidAmount; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 2, input, output, bidder, reward); - return output; - } - - NOST::PlaceBid_output placeBatchBidWithRequiredReward(const id& bidder, uint64 auctionIndex, uint64 bidQuantity, uint64 bidAmount) - { - const NOST::CalculateBatchAuctionBidFee_output& calculation = calculateBatchAuctionBidFee(bidQuantity, bidAmount); - return placeBid(bidder, auctionIndex, bidQuantity, bidAmount, static_cast(calculation.requiredReward)); - } - - NOST::PlaceBid_output placeBatchBidWithFundedRequiredReward(const id& bidder, uint64 auctionIndex, uint64 bidQuantity, uint64 bidAmount) - { - const NOST::CalculateBatchAuctionBidFee_output& calculation = calculateBatchAuctionBidFee(bidQuantity, bidAmount); - return placeBidWithFundedReward(bidder, auctionIndex, bidQuantity, bidAmount, static_cast(calculation.requiredReward)); - } - - NOST::CancelAuction_output cancelAuction(const id& seller, uint64 auctionIndex, sint64 reward) - { - NOST::CancelAuction_input input{}; - NOST::CancelAuction_output output{}; - - input.auctionIndex = auctionIndex; - if (reward > 0) - { - seedUser(seller, reward); - } - else - { - ensureUser(seller); - } - invokeUserProcedure(NOST_CONTRACT_INDEX, 3, input, output, seller, reward); - return output; - } - - NOST::TransferShareManagementRights_output transferManagedSharesWithReward(const id& owner, const Asset& asset, sint64 numberOfShares, - uint32 contractIndex, sint64 reward) - { - if (reward > 0) - { - seedUser(owner, reward); - } - else - { - ensureUser(owner); - } - return transferManagedSharesWithFundedReward(owner, asset, numberOfShares, contractIndex, reward); - } - - NOST::TransferShareManagementRights_output transferManagedSharesWithFundedReward(const id& owner, const Asset& asset, sint64 numberOfShares, - uint32 contractIndex, sint64 reward) - { - NOST::TransferShareManagementRights_input input{}; - NOST::TransferShareManagementRights_output output{}; - - input.asset = asset; - input.numberOfShares = numberOfShares; - input.newManagingContractIndex = contractIndex; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 4, input, output, owner, reward); - return output; - } - - NOST::TransferShareManagementRights_output transferManagedShares(const id& owner, const Asset& asset, sint64 numberOfShares, uint32 contractIndex) - { - syncCachedQxTransferFee(); - return transferManagedSharesWithReward(owner, asset, numberOfShares, contractIndex, getCachedQxTransferFee()); - } - - NOST::ResolvePendingStandardAuction_output resolvePendingStandardAuction(const id& seller, uint64 auctionIndex, bool acceptSale) - { - NOST::ResolvePendingStandardAuction_input input{}; - NOST::ResolvePendingStandardAuction_output output{}; - - input.auctionIndex = auctionIndex; - input.acceptSale = acceptSale ? 1 : 0; - - ensureUser(seller); - invokeUserProcedure(NOST_CONTRACT_INDEX, 5, input, output, seller, 0); - return output; - } - - NOST::SetAuctionFees_output setAuctionFees(const id& caller, const NOST::SetAuctionFees_input& input) - { - NOST::SetAuctionFees_output output{}; - ensureUser(caller); - invokeUserProcedure(NOST_CONTRACT_INDEX, 6, input, output, caller, 0); - return output; - } - - NOST::SetAuctionFeesByManagement_output setAuctionFeesByManagement(const id& caller, const NOST::SetAuctionFeesByManagement_input& input) - { - NOST::SetAuctionFeesByManagement_output output{}; - ensureUser(caller); - invokeUserProcedure(NOST_CONTRACT_INDEX, 7, input, output, caller, 0); - return output; - } - - NOST::SetManagement_output setManagement(const id& caller, const id& management) - { - NOST::SetManagement_input input{}; - NOST::SetManagement_output output{}; - - input.management = management; - ensureUser(caller); - invokeUserProcedure(NOST_CONTRACT_INDEX, 8, input, output, caller, 0); - return output; - } - - NOST::GetAuctionByIndex_output getAuction(uint64 auctionIndex) const - { - NOST::GetAuctionByIndex_input input{}; - NOST::GetAuctionByIndex_output output{}; - - input.auctionIndex = auctionIndex; - callFunction(NOST_CONTRACT_INDEX, 1, input, output); - return output; - } - - NOST::GetAuctionParticipant_output getParticipant(uint64 auctionIndex, const id& participant) const - { - NOST::GetAuctionParticipant_input input{}; - NOST::GetAuctionParticipant_output output{}; - - input.auctionIndex = auctionIndex; - input.participant = participant; - callFunction(NOST_CONTRACT_INDEX, 2, input, output); - return output; - } - - NOST::GetTicksBeforeAuctionLaunch_output getTicksBeforeAuctionLaunch() const - { - NOST::GetTicksBeforeAuctionLaunch_input input{}; - NOST::GetTicksBeforeAuctionLaunch_output output{}; - - callFunction(NOST_CONTRACT_INDEX, 3, input, output); - return output; - } - - NOST::GetAuctionFees_output getAuctionFees() const - { - NOST::GetAuctionFees_input input{}; - NOST::GetAuctionFees_output output{}; - - callFunction(NOST_CONTRACT_INDEX, 4, input, output); - return output; - } - - NOST::SetAuctionFees_input makeCoordinatorFeeInput(sint64 publicAuctionCreationFee) const - { - const NOST::GetAuctionFees_output& fees = getAuctionFees(); - NOST::SetAuctionFees_input input{}; - input.privateAuctionFee = fees.privateAuctionFee; - input.publicAuctionCreationFee = publicAuctionCreationFee; - input.auctionCancellationFeeBasisPoints = fees.auctionCancellationFeeBasisPoints; - input.managementFeeBasisPoints = fees.managementFeeBasisPoints; - input.developmentFeeBasisPoints = fees.developmentFeeBasisPoints; - input.takeoverCoordinatorFeeBasisPoints = fees.takeoverCoordinatorFeeBasisPoints; - input.shareholderDividendBasisPoints = fees.shareholderDividendBasisPoints; - input.shareholderFeeBasisPointsTier1 = fees.shareholderFeeBasisPointsTier1; - input.shareholderFeeBasisPointsTier2 = fees.shareholderFeeBasisPointsTier2; - input.shareholderFeeBasisPointsTier3 = fees.shareholderFeeBasisPointsTier3; - input.shareholderFeeBasisPointsTier4 = fees.shareholderFeeBasisPointsTier4; - return input; - } - - NOST::SetAuctionFeesByManagement_input makeManagementFeeInput(sint64 publicAuctionCreationFee) const - { - const NOST::GetAuctionFees_output& fees = getAuctionFees(); - NOST::SetAuctionFeesByManagement_input input{}; - input.privateAuctionFee = fees.privateAuctionFee; - input.publicAuctionCreationFee = publicAuctionCreationFee; - input.auctionCancellationFeeBasisPoints = fees.auctionCancellationFeeBasisPoints; - input.managementFeeBasisPoints = fees.managementFeeBasisPoints; - input.developmentFeeBasisPoints = fees.developmentFeeBasisPoints; - input.shareholderFeeBasisPointsTier1 = fees.shareholderFeeBasisPointsTier1; - input.shareholderFeeBasisPointsTier2 = fees.shareholderFeeBasisPointsTier2; - input.shareholderFeeBasisPointsTier3 = fees.shareholderFeeBasisPointsTier3; - input.shareholderFeeBasisPointsTier4 = fees.shareholderFeeBasisPointsTier4; - return input; - } - - NOST::CalculateBatchAuctionBidFee_output calculateBatchAuctionBidFee(uint64 bidQuantity, uint64 bidAmount) const - { - NOST::CalculateBatchAuctionBidFee_input input{}; - NOST::CalculateBatchAuctionBidFee_output output{}; - - input.bidQuantity = bidQuantity; - input.bidAmount = bidAmount; - callFunction(NOST_CONTRACT_INDEX, 20, input, output); - return output; - } - - NOST::GetFeeRecipients_output getFeeRecipients() const - { - NOST::GetFeeRecipients_input input{}; - NOST::GetFeeRecipients_output output{}; - - callFunction(NOST_CONTRACT_INDEX, 5, input, output); - return output; - } - - NOST::GetClosedAuctionHistory_output getClosedAuctionHistory() const - { - NOST::GetClosedAuctionHistory_input input{}; - NOST::GetClosedAuctionHistory_output output{}; - - callFunction(NOST_CONTRACT_INDEX, 6, input, output); - return output; - } - - NOST::GetRouteAllFeesToDevelopment_output getRouteAllFeesToDevelopmentPublic() const - { - NOST::GetRouteAllFeesToDevelopment_input input{}; - NOST::GetRouteAllFeesToDevelopment_output output{}; - - callFunction(NOST_CONTRACT_INDEX, 7, input, output); - return output; - } - - NOST::GetContractStats_output getContractStats() const - { - NOST::GetContractStats_input input{}; - NOST::GetContractStats_output output{}; - - callFunction(NOST_CONTRACT_INDEX, 8, input, output); - return output; - } - - NOST::GetAuctionSummaries_output getAuctionSummaries(uint64 offset, uint64 limit) const - { - NOST::GetAuctionSummaries_input input{}; - NOST::GetAuctionSummaries_output output{}; - - input.offset = offset; - input.limit = limit; - callFunction(NOST_CONTRACT_INDEX, 9, input, output); - return output; - } - - NOST::GetActiveAuctionIndices_output getActiveAuctionIndices(uint64 offset, uint64 limit) const - { - NOST::GetActiveAuctionIndices_input input{}; - NOST::GetActiveAuctionIndices_output output{}; - - input.offset = offset; - input.limit = limit; - callFunction(NOST_CONTRACT_INDEX, 10, input, output); - return output; - } - - NOST::GetAuctionsBySeller_output getAuctionsBySeller(const id& seller, uint64 offset, uint64 limit) const - { - NOST::GetAuctionsBySeller_input input{}; - NOST::GetAuctionsBySeller_output output{}; - - input.seller = seller; - input.offset = offset; - input.limit = limit; - callFunction(NOST_CONTRACT_INDEX, 11, input, output); - return output; - } - - NOST::GetAuctionByMetadataCid_output getAuctionByMetadataCid(const Array& metadataCid) const - { - NOST::GetAuctionByMetadataCid_input input{}; - NOST::GetAuctionByMetadataCid_output output{}; - - input.metadataIpfsCid = metadataCid; - callFunction(NOST_CONTRACT_INDEX, 12, input, output); - return output; - } - - NOST::GetAuctionSummariesByIndexBatch_output getAuctionSummariesByIndexBatch(const Array& auctionIndices, - uint64 count) const - { - NOST::GetAuctionSummariesByIndexBatch_input input{}; - NOST::GetAuctionSummariesByIndexBatch_output output{}; - - input.auctionIndices = auctionIndices; - input.count = count; - callFunction(NOST_CONTRACT_INDEX, 13, input, output); - return output; - } - - NOST::GetAuctionParticipants_output getAuctionParticipants(uint64 auctionIndex, uint64 offset, uint64 limit) const - { - NOST::GetAuctionParticipants_input input{}; - NOST::GetAuctionParticipants_output output{}; - - input.auctionIndex = auctionIndex; - input.offset = offset; - input.limit = limit; - callFunction(NOST_CONTRACT_INDEX, 14, input, output); - return output; - } - - NOST::GetUserParticipations_output getUserParticipations(const id& participant, uint64 offset, uint64 limit) const - { - NOST::GetUserParticipations_input input{}; - NOST::GetUserParticipations_output output{}; - - input.participant = participant; - input.offset = offset; - input.limit = limit; - callFunction(NOST_CONTRACT_INDEX, 15, input, output); - return output; - } - - NOST::GetLatestAuctionIndex_output getLatestAuctionIndex() const - { - NOST::GetLatestAuctionIndex_input input{}; - NOST::GetLatestAuctionIndex_output output{}; - - callFunction(NOST_CONTRACT_INDEX, 16, input, output); - return output; - } - - NOST::GetAuctionCountBySeller_output getAuctionCountBySeller(const id& seller) const - { - NOST::GetAuctionCountBySeller_input input{}; - NOST::GetAuctionCountBySeller_output output{}; - - input.seller = seller; - callFunction(NOST_CONTRACT_INDEX, 17, input, output); - return output; - } - - NOST::GetAuctionAtCreationSnapshot_output getAuctionAtCreationSnapshot(uint64 auctionIndex) const - { - NOST::GetAuctionAtCreationSnapshot_input input{}; - NOST::GetAuctionAtCreationSnapshot_output output{}; - - input.auctionIndex = auctionIndex; - callFunction(NOST_CONTRACT_INDEX, 18, input, output); - return output; - } - - NOST::GetBatchAuctionBidAvailability_output getBatchAvailability(uint64 auctionIndex) const - { - NOST::GetBatchAuctionBidAvailability_input input{}; - NOST::GetBatchAuctionBidAvailability_output output{}; - - input.auctionIndex = auctionIndex; - callFunction(NOST_CONTRACT_INDEX, 19, input, output); - return output; - } - - NOST::GetPendingServiceFeePool_output getPendingServiceFeePool() const - { - NOST::GetPendingServiceFeePool_input input{}; - NOST::GetPendingServiceFeePool_output output{}; - - callFunction(NOST_CONTRACT_INDEX, 21, input, output); - return output; - } - - NOST::GetFeeReserveGuardState_output getFeeReserveGuardState() const - { - NOST::GetFeeReserveGuardState_input input{}; - NOST::GetFeeReserveGuardState_output output{}; - - callFunction(NOST_CONTRACT_INDEX, 22, input, output); - return output; - } - - NOST::GetPendingPayout_output getPendingPayout(const id& account) const - { - NOST::GetPendingPayout_input input{}; - NOST::GetPendingPayout_output output{}; - input.account = account; - callFunction(NOST_CONTRACT_INDEX, 23, input, output); - return output; - } - - NOST::GetNostromoFeePool_output getNostromoFeePool() const - { - NOST::GetNostromoFeePool_input input{}; - NOST::GetNostromoFeePool_output output{}; - - callFunction(NOST_CONTRACT_INDEX, 24, input, output); - return output; - } - - NOST::SetFeeReserveGuardConfig_output setFeeReserveGuardConfig(const id& caller, uint64 dropBasisPoints, uint64 windowSeconds) - { - NOST::SetFeeReserveGuardConfig_input input{}; - NOST::SetFeeReserveGuardConfig_output output{}; - - input.dropBasisPoints = dropBasisPoints; - input.windowSeconds = windowSeconds; - ensureUser(caller); - invokeUserProcedure(NOST_CONTRACT_INDEX, 9, input, output, caller, 0); - return output; - } - - NOST::SetEmergencyPause_output setEmergencyPause(const id& caller, bool paused) - { - NOST::SetEmergencyPause_input input{}; - NOST::SetEmergencyPause_output output{}; - - input.paused = paused ? 1 : 0; - ensureUser(caller); - invokeUserProcedure(NOST_CONTRACT_INDEX, 10, input, output, caller, 0); - return output; - } - - NOST::StateData& stateData() { return *reinterpret_cast(contractStates[NOST_CONTRACT_INDEX]); } - const NOST::StateData& stateData() const { return *reinterpret_cast(contractStates[NOST_CONTRACT_INDEX]); } - QX::StateData& qxStateData() { return *reinterpret_cast(contractStates[QX_CONTRACT_INDEX]); } - - void setRouteAllFeesToDevelopment(uint8 enabled) { stateData().routeAllFeesToDevelopment = enabled; } - uint8 getRouteAllFeesToDevelopment() const { return stateData().routeAllFeesToDevelopment; } - void syncCachedQxTransferFee() { stateData().qxTransferFee = qxStateData()._transferFee; } - uint32 getCachedQxTransferFee() const { return stateData().qxTransferFee; } - - sint64 managedShares(const Asset& asset, const id& owner) const - { - return numberOfPossessedShares(asset.assetName, asset.issuer, owner, owner, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX); - } - - sint64 sharesManagedBy(const Asset& asset, const id& owner, uint32 contractIndex) const - { - return numberOfPossessedShares(asset.assetName, asset.issuer, owner, owner, contractIndex, contractIndex); - } - - sint64 plainShares(const Asset& asset, const id& owner) const - { - return numberOfShares(asset, AssetOwnershipSelect::byOwner(owner), AssetPossessionSelect::byPossessor(owner)); - } - - static Array makeMetadataCid() - { - Array cid{}; - const char* cidText = "bafybeigdyrzt2a3x4m5n6p7qrstuvwx234567abcdefghijklmnopqrst"; - for (uint64 i = 0; cidText[i] != 0 && i < NOST_AUCTION_METADATA_CID_LENGTH; ++i) - { - cid.set(i, static_cast(cidText[i])); - } - return cid; - } - - static Array makeInvalidMetadataCidFirstChar() - { - auto cid = makeMetadataCid(); - cid.set(0, 'c'); - return cid; - } - - static Array makeInvalidMetadataCidUppercase() - { - auto cid = makeMetadataCid(); - cid.set(5, 'A'); - return cid; - } - - static Array makeSingleLot(const Asset& asset, sint64 quantity) - { - Array lot{}; - NOST::AuctionAssetEntry entry{}; - - entry.asset = asset; - entry.quantity = quantity; - lot.set(0, entry); - return lot; - } - - static Array makeLot(std::initializer_list entries) - { - Array lot{}; - uint64 index = 0; - for (const auto& entry : entries) - { - lot.set(index++, entry); - } - return lot; - } - - static Array makeAllowedWallets(std::initializer_list wallets) - { - Array allowed{}; - uint64 index = 0; - for (const auto& wallet : wallets) - { - allowed.set(index++, wallet); - } - return allowed; - } - - static Array - makeRequiredAccessAssets(std::initializer_list assets) - { - Array required{}; - uint64 index = 0; - for (const auto& asset : assets) - { - required.set(index++, asset); - } - return required; - } - - static NOST::CreateAuction_input makeBatchAuctionInput(const Asset& asset, sint64 quantity, uint64 salePrice = 10) - { - NOST::CreateAuction_input input{}; - input.metadataIpfsCid = makeMetadataCid(); - input.auctionLotItems = makeSingleLot(asset, quantity); - input.minimumPurchaseQuantity = 1; - input.salePrice = salePrice; - input.durationDays = 1; - input.auctionType = static_cast(NOST::EAuctionType::Batch); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Public); - return input; - } - - static NOST::CreateAuction_input makeStandardAuctionInput(const Array& lot, - uint64 initialPrice = NOST_STANDARD_MIN_PRICE, - uint64 salePrice = NOST_STANDARD_MIN_PRICE, - uint64 minimumBidIncrement = NOST_STANDARD_MIN_BID_INCREMENT, uint64 buyNowPrice = 0) - { - NOST::CreateAuction_input input{}; - input.metadataIpfsCid = makeMetadataCid(); - input.auctionLotItems = lot; - input.minimumPurchaseQuantity = 1; - input.initialPrice = initialPrice; - input.salePrice = salePrice; - input.minimumBidIncrement = minimumBidIncrement; - input.buyNowPrice = buyNowPrice; - input.durationDays = 1; - input.auctionType = static_cast(NOST::EAuctionType::Standard); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Public); - return input; - } - - sint64 expectedDividendPoolIncrease(uint64 addedDividendAmount) const - { - const uint64 poolBefore = stateData().auctionShareholderDividendPool; - const uint64 poolAfterFunding = poolBefore + addedDividendAmount; - return static_cast(poolAfterFunding % NUMBER_OF_COMPUTORS) - static_cast(poolBefore); - } - - static id managementWallet() - { - return ID(_I, _G, _P, _Z, _X, _Q, _O, _R, _J, _Y, _Q, _P, _A, _G, _V, _A, _B, _N, _T, _N, _I, _S, _O, _Y, _T, _M, _T, _A, _N, _M, _K, _Z, _A, - _S, _T, _P, _P, _G, _Z, _O, _N, _A, _Q, _J, _X, _Q, _O, _S, _W, _Q, _O, _V, _J, _C, _K, _D); - } - - static id developmentWallet() - { - return ID(_D, _Q, _V, _H, _M, _Z, _F, _C, _W, _O, _K, _M, _H, _F, _B, _H, _L, _X, _U, _I, _U, _G, _P, _P, _X, _R, _Z, _C, _U, _V, _S, _N, _J, - _F, _Z, _J, _F, _M, _Q, _M, _Y, _D, _B, _X, _E, _S, _E, _A, _T, _M, _W, _L, _K, _N, _L, _D); - } - - static id takeoverCoordinatorWallet() - { - return ID(_X, _J, _O, _S, _N, _L, _T, _Z, _V, _V, _H, _N, _Z, _C, _B, _Y, _X, _I, _E, _V, _N, _E, _P, _P, _B, _O, _Q, _A, _W, _D, _B, _V, _G, - _E, _N, _Z, _O, _X, _S, _V, _O, _B, _K, _G, _Z, _C, _C, _F, _D, _B, _D, _M, _T, _M, _L, _C); - } -}; - -static bool containsWallet(const Array& wallets, uint64 count, const id& wallet) -{ - for (uint64 index = 0; index < count; ++index) - { - if (wallets.get(index) == wallet) - { - return true; - } - } - return false; -} - -static bool containsAccessAsset(const Array& assets, uint64 count, - const NOST::AuctionAssetEntry& expected) -{ - for (uint64 index = 0; index < count; ++index) - { - if (assets.get(index).asset == expected.asset && assets.get(index).quantity == expected.quantity) - { - return true; - } - } - return false; -} - -static bool containsAuctionIndex(const Array& auctionIndices, uint64 count, uint64 auctionIndex) -{ - const uint64 boundedCount = count < auctionIndices.capacity() ? count : auctionIndices.capacity(); - for (uint64 index = 0; index < boundedCount; ++index) - { - if (auctionIndices.get(index) == auctionIndex) - { - return true; - } - } - return false; -} - -static void expectAuctionFeesEqual(const NOST::GetAuctionFees_output& actual, const NOST::GetAuctionFees_output& expected) -{ - EXPECT_EQ(actual.privateAuctionFee, expected.privateAuctionFee); - EXPECT_EQ(actual.publicAuctionCreationFee, expected.publicAuctionCreationFee); - EXPECT_EQ(actual.auctionCancellationFeeBasisPoints, expected.auctionCancellationFeeBasisPoints); - EXPECT_EQ(actual.managementFeeBasisPoints, expected.managementFeeBasisPoints); - EXPECT_EQ(actual.developmentFeeBasisPoints, expected.developmentFeeBasisPoints); - EXPECT_EQ(actual.takeoverCoordinatorFeeBasisPoints, expected.takeoverCoordinatorFeeBasisPoints); - EXPECT_EQ(actual.shareholderDividendBasisPoints, expected.shareholderDividendBasisPoints); - EXPECT_EQ(actual.shareholderFeeBasisPointsTier1, expected.shareholderFeeBasisPointsTier1); - EXPECT_EQ(actual.shareholderFeeBasisPointsTier2, expected.shareholderFeeBasisPointsTier2); - EXPECT_EQ(actual.shareholderFeeBasisPointsTier3, expected.shareholderFeeBasisPointsTier3); - EXPECT_EQ(actual.shareholderFeeBasisPointsTier4, expected.shareholderFeeBasisPointsTier4); -} - -TEST(ContractNostromoAuction, InitialStateAndGettersAuction) -{ - ContractTestingNOST nostromo; - - const auto fees = nostromo.getAuctionFees(); - EXPECT_EQ(fees.privateAuctionFee, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - EXPECT_EQ(fees.publicAuctionCreationFee, NOST_PUBLIC_AUCTION_CREATION_FEE); - EXPECT_EQ(fees.auctionCancellationFeeBasisPoints, NOST_DEFAULT_AUCTION_CANCELLATION_FEE_BP); - EXPECT_EQ(fees.managementFeeBasisPoints, NOST_DEFAULT_AUCTION_MANAGEMENT_FEE_BP); - EXPECT_EQ(fees.developmentFeeBasisPoints, NOST_DEFAULT_AUCTION_DEVELOPMENT_FEE_BP); - EXPECT_EQ(fees.takeoverCoordinatorFeeBasisPoints, NOST_DEFAULT_AUCTION_TAKEOVER_COORDINATOR_FEE_BP); - EXPECT_EQ(fees.shareholderDividendBasisPoints, NOST_DEFAULT_AUCTION_SHAREHOLDER_DIVIDEND_BP); - EXPECT_EQ(fees.shareholderFeeBasisPointsTier1, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_1); - EXPECT_EQ(fees.shareholderFeeBasisPointsTier2, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_2); - EXPECT_EQ(fees.shareholderFeeBasisPointsTier3, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_3); - EXPECT_EQ(fees.shareholderFeeBasisPointsTier4, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4); - - const auto recipients = nostromo.getFeeRecipients(); - EXPECT_EQ(recipients.management, ContractTestingNOST::managementWallet()); - EXPECT_EQ(recipients.development, ContractTestingNOST::developmentWallet()); - EXPECT_EQ(recipients.takeoverCoordinator, ContractTestingNOST::takeoverCoordinatorWallet()); - - const uint64 missingAuction = 777; - const id missingParticipant(888, 0, 0, 0); - const auto auctionOutput = nostromo.getAuction(missingAuction); - const auto participantOutput = nostromo.getParticipant(missingAuction, missingParticipant); - const auto launchPause = nostromo.getTicksBeforeAuctionLaunch(); - - EXPECT_EQ(auctionOutput.auction.core.auctionIndex, 0ULL); - EXPECT_EQ(participantOutput.found, 0); - EXPECT_EQ(launchPause.ticks, 0U); - EXPECT_EQ(nostromo.getClosedAuctionHistory().totalEntries, 0ULL); - EXPECT_EQ(nostromo.getRouteAllFeesToDevelopmentPublic().enabled, NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT); - - nostromo.setRouteAllFeesToDevelopment(1); - EXPECT_EQ(nostromo.getRouteAllFeesToDevelopmentPublic().enabled, 1); - nostromo.setRouteAllFeesToDevelopment(0); - EXPECT_EQ(nostromo.getRouteAllFeesToDevelopmentPublic().enabled, 0); - - nostromo.beginEpoch(); - EXPECT_EQ(nostromo.getCachedQxTransferFee(), nostromo.qxStateData()._transferFee); - EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); - nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); - EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, 0U); -} - -TEST(ContractNostromoAuction, AuctionIndexAndExpandedGetterSurfaceAuction) -{ - ContractTestingNOST nostromo; - const id sellerA(31, 32, 33, 34); - const id sellerB(35, 36, 37, 38); - const id bidderA(39, 40, 41, 42); - const id bidderB(43, 44, 45, 46); - const uint64 assetNameA = assetNameFromString("IDXGTA"); - const uint64 assetNameB = assetNameFromString("IDXGTB"); - const uint64 assetNameC = assetNameFromString("IDXGTC"); - const Asset assetA{sellerA, assetNameA}; - const Asset assetB{sellerA, assetNameB}; - const Asset assetC{sellerB, assetNameC}; - - EXPECT_EQ(nostromo.getLatestAuctionIndex().found, 0); - EXPECT_EQ(nostromo.getLatestAuctionIndex().auctionIndex, 0ULL); - - EXPECT_EQ(nostromo.issueAsset(sellerA, assetNameA, 3), 3); - EXPECT_EQ(nostromo.issueAsset(sellerA, assetNameB, 2), 2); - EXPECT_EQ(nostromo.issueAsset(sellerB, assetNameC, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(sellerA, assetA, 3), 3); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(sellerA, assetB, 2), 2); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(sellerB, assetC, 1), 1); - - auto inputA = ContractTestingNOST::makeBatchAuctionInput(assetA, 3, 10); - auto inputB = ContractTestingNOST::makeBatchAuctionInput(assetB, 2, 12); - auto inputC = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetC, 1)); - inputB.metadataIpfsCid.set(10, '2'); - inputC.metadataIpfsCid.set(10, '3'); - - const auto createA = nostromo.createAuction(sellerA, inputA); - const auto createB = nostromo.createAuction(sellerA, inputB); - const auto createC = nostromo.createAuction(sellerB, inputC); - ASSERT_EQ(createA.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(createB.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(createC.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(createA.auctionIndex, 0ULL); - EXPECT_EQ(createB.auctionIndex, 1ULL); - EXPECT_EQ(createC.auctionIndex, 2ULL); - - EXPECT_EQ(nostromo.getLatestAuctionIndex().found, 1); - EXPECT_EQ(nostromo.getLatestAuctionIndex().auctionIndex, 2ULL); - EXPECT_EQ(nostromo.getAuction(createB.auctionIndex).auction.core.auctionIndex, 1ULL); - EXPECT_EQ(nostromo.getAuctionAtCreationSnapshot(createC.auctionIndex).seller, sellerB); - EXPECT_EQ(nostromo.getAuctionAtCreationSnapshot(createC.auctionIndex).auctionIndex, 2ULL); - - const auto summaries = nostromo.getAuctionSummaries(0, 64); - EXPECT_EQ(summaries.totalCount, 3ULL); - EXPECT_EQ(summaries.returnedCount, 3ULL); - EXPECT_EQ(summaries.auctions.get(0).auctionIndex, 0ULL); - EXPECT_EQ(summaries.auctions.get(2).seller, sellerB); - - const auto sellerAList = nostromo.getAuctionsBySeller(sellerA, 0, 64); - EXPECT_EQ(sellerAList.totalCount, 2ULL); - EXPECT_EQ(sellerAList.returnedCount, 2ULL); - EXPECT_EQ(nostromo.getAuctionCountBySeller(sellerA).count, 2ULL); - EXPECT_EQ(nostromo.getAuctionCountBySeller(sellerB).count, 1ULL); - - const auto metadataLookup = nostromo.getAuctionByMetadataCid(inputB.metadataIpfsCid); - EXPECT_EQ(metadataLookup.found, 1); - EXPECT_EQ(metadataLookup.auctionIndex, 1ULL); - EXPECT_EQ(metadataLookup.auction.seller, sellerA); - - Array requestedIndices{}; - requestedIndices.set(0, createC.auctionIndex); - requestedIndices.set(1, 999); - requestedIndices.set(2, createA.auctionIndex); - const auto batch = nostromo.getAuctionSummariesByIndexBatch(requestedIndices, 3); - EXPECT_EQ(batch.returnedCount, 2ULL); - EXPECT_EQ(batch.found.get(0), 1); - EXPECT_EQ(batch.found.get(1), 0); - EXPECT_EQ(batch.found.get(2), 1); - EXPECT_EQ(batch.auctions.get(0).auctionIndex, 2ULL); - EXPECT_EQ(batch.auctions.get(2).auctionIndex, 0ULL); - - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createA.auctionIndex, 2, 11).errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderB, createA.auctionIndex, 1, 15).errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(bidderA, createC.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, - NOST::EAuctionError::Success); - - const auto active = nostromo.getActiveAuctionIndices(0, 64); - EXPECT_EQ(active.totalCount, 3ULL); - EXPECT_EQ(active.returnedCount, 3ULL); - EXPECT_EQ(active.auctionIndices.get(1), 1ULL); - - const auto participants = nostromo.getAuctionParticipants(createA.auctionIndex, 0, 64); - EXPECT_EQ(participants.totalCount, 2ULL); - EXPECT_EQ(participants.returnedCount, 2ULL); - EXPECT_TRUE(participants.participants.get(0).participant == bidderA || participants.participants.get(1).participant == bidderA); - - const auto bidderAParticipations = nostromo.getUserParticipations(bidderA, 0, 64); - EXPECT_EQ(bidderAParticipations.totalCount, 2ULL); - EXPECT_EQ(bidderAParticipations.returnedCount, 2ULL); - - const auto stats = nostromo.getContractStats(); - EXPECT_EQ(stats.stats.totalAuctionsCreated, 3ULL); - EXPECT_EQ(stats.stats.activeAuctionCount, 3ULL); - EXPECT_EQ(stats.stats.participantCount, 3ULL); -} - -TEST(ContractNostromoAuction, RetainedAuctionGetterPaginationAcrossLiveAndClosedStorageAuction) -{ - ContractTestingNOST nostromo; - const id sellerA(61, 62, 63, 64); - const id sellerB(65, 66, 67, 68); - const id missingSeller(69, 70, 71, 72); - const auto makeAuction = - [](uint64 auctionIndex, const id& seller, NOST::EAuctionStatus status, const Array& metadataCid) - { - NOST::AuctionData auction{}; - auction.core.auctionIndex = auctionIndex; - auction.core.seller = seller; - auction.core.status = status; - auction.core.metadataIpfsCid = metadataCid; - return auction; - }; - auto sharedCid = ContractTestingNOST::makeMetadataCid(); - auto cidAtIndex1 = ContractTestingNOST::makeMetadataCid(); - auto cidAtIndex5 = ContractTestingNOST::makeMetadataCid(); - auto cidAtIndex6 = ContractTestingNOST::makeMetadataCid(); - auto cidAtIndex7 = ContractTestingNOST::makeMetadataCid(); - auto missingCid = ContractTestingNOST::makeMetadataCid(); - sharedCid.set(10, 's'); - cidAtIndex1.set(10, 'a'); - cidAtIndex5.set(10, 'c'); - cidAtIndex6.set(10, 'd'); - cidAtIndex7.set(10, 'e'); - missingCid.set(10, 'm'); - - // Insert live auctions out of creation order to ensure pagination does not depend on physical hash-map order. - ASSERT_NE(nostromo.stateData().auctionList.set(7, makeAuction(7, sellerA, NOST::EAuctionStatus::Active, cidAtIndex7)), NULL_INDEX); - ASSERT_NE(nostromo.stateData().auctionList.set(1, makeAuction(1, sellerB, NOST::EAuctionStatus::Active, cidAtIndex1)), NULL_INDEX); - ASSERT_NE(nostromo.stateData().auctionList.set(9, makeAuction(9, sellerA, NOST::EAuctionStatus::Active, sharedCid)), NULL_INDEX); - ASSERT_NE(nostromo.stateData().auctionList.set(5, makeAuction(5, sellerA, NOST::EAuctionStatus::PendingSellerDecision, cidAtIndex5)), NULL_INDEX); - - // A partially filled history verifies that uninitialized ring capacity is not scanned as retained data. - nostromo.stateData().closedAuctionHistory.set(0, makeAuction(2, sellerA, NOST::EAuctionStatus::Finalized, sharedCid)); - nostromo.stateData().closedAuctionHistory.set(1, makeAuction(6, sellerB, NOST::EAuctionStatus::Cancelled, cidAtIndex6)); - nostromo.stateData().closedAuctionHistoryCounter = 2; - - const auto summaries = nostromo.getAuctionSummaries(1, 3); - ASSERT_EQ(summaries.totalCount, 6ULL); - ASSERT_EQ(summaries.returnedCount, 3ULL); - EXPECT_EQ(summaries.auctions.get(0).auctionIndex, 2ULL); - EXPECT_EQ(summaries.auctions.get(1).auctionIndex, 5ULL); - EXPECT_EQ(summaries.auctions.get(2).auctionIndex, 6ULL); - - const auto active = nostromo.getActiveAuctionIndices(1, 2); - ASSERT_EQ(active.totalCount, 4ULL); - ASSERT_EQ(active.returnedCount, 2ULL); - EXPECT_EQ(active.auctionIndices.get(0), 5ULL); - EXPECT_EQ(active.auctionIndices.get(1), 7ULL); - EXPECT_EQ(nostromo.getActiveAuctionIndices(0, 0).returnedCount, 0ULL); - EXPECT_EQ(nostromo.getActiveAuctionIndices(active.totalCount, 2).returnedCount, 0ULL); - - const auto sellerPage = nostromo.getAuctionsBySeller(sellerA, 1, 2); - ASSERT_EQ(sellerPage.totalCount, 4ULL); - ASSERT_EQ(sellerPage.returnedCount, 2ULL); - EXPECT_EQ(sellerPage.auctions.get(0).auctionIndex, 5ULL); - EXPECT_EQ(sellerPage.auctions.get(1).auctionIndex, 7ULL); - EXPECT_EQ(nostromo.getAuctionCountBySeller(sellerA).count, sellerPage.totalCount); - EXPECT_EQ(nostromo.getAuctionCountBySeller(sellerB).count, 2ULL); - EXPECT_EQ(nostromo.getAuctionCountBySeller(missingSeller).count, 0ULL); - EXPECT_EQ(nostromo.getAuctionsBySeller(missingSeller, 0, 2).returnedCount, 0ULL); - - const auto sharedCidLookup = nostromo.getAuctionByMetadataCid(sharedCid); - ASSERT_EQ(sharedCidLookup.found, 1); - EXPECT_EQ(sharedCidLookup.auctionIndex, 2ULL); - EXPECT_EQ(sharedCidLookup.auction.seller, sellerA); - EXPECT_EQ(nostromo.getAuctionByMetadataCid(missingCid).found, 0); -} - -TEST(ContractNostromoAuction, TransferShareManagementRightsAuction) -{ - ContractTestingNOST nostromo; - const id owner(1, 2, 3, 4); - const uint64 assetName = assetNameFromString("NOSTTR"); - const Asset asset{owner, assetName}; - - EXPECT_EQ(nostromo.issueAsset(owner, assetName, 10), 10); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(owner, asset, 7), 7); - EXPECT_EQ(nostromo.managedShares(asset, owner), 7); - EXPECT_EQ(nostromo.sharesManagedBy(asset, owner, QX_CONTRACT_INDEX), 3); - - const auto invalidZeroShares = nostromo.transferManagedShares(owner, asset, 0, QX_CONTRACT_INDEX); - EXPECT_EQ(invalidZeroShares.transferredNumberOfShares, 0); - EXPECT_EQ(invalidZeroShares.errorCode, NOST::EAuctionError::InvalidInput); - - Asset zeroAsset{}; - const auto invalidZeroAsset = nostromo.transferManagedShares(owner, zeroAsset, 1, QX_CONTRACT_INDEX); - EXPECT_EQ(invalidZeroAsset.transferredNumberOfShares, 0); - EXPECT_EQ(invalidZeroAsset.errorCode, NOST::EAuctionError::InvalidInput); - - const auto invalidZeroContract = nostromo.transferManagedShares(owner, asset, 1, 0); - EXPECT_EQ(invalidZeroContract.transferredNumberOfShares, 0); - EXPECT_EQ(invalidZeroContract.errorCode, NOST::EAuctionError::InvalidInput); - - const auto insufficient = nostromo.transferManagedShares(owner, asset, 8, QX_CONTRACT_INDEX); - EXPECT_EQ(insufficient.transferredNumberOfShares, 0); - EXPECT_EQ(insufficient.errorCode, NOST::EAuctionError::InvalidInput); - - const auto success = nostromo.transferManagedShares(owner, asset, 5, QX_CONTRACT_INDEX); - EXPECT_EQ(success.transferredNumberOfShares, 5); - EXPECT_EQ(success.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.managedShares(asset, owner), 2); - EXPECT_EQ(nostromo.sharesManagedBy(asset, owner, QX_CONTRACT_INDEX), 8); -} - -TEST(ContractNostromoAuction, TransferShareManagementRightsRequiresInvocationRewardAuction) -{ - { - ContractTestingNOST nostromo; - const id owner(5, 6, 7, 8); - const uint64 assetName = assetNameFromString("TRFEXA"); - const Asset asset{owner, assetName}; - - EXPECT_EQ(nostromo.issueAsset(owner, assetName, 4), 4); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(owner, asset, 4), 4); - nostromo.syncCachedQxTransferFee(); - - const auto output = nostromo.transferManagedSharesWithReward(owner, asset, 2, QX_CONTRACT_INDEX, nostromo.getCachedQxTransferFee()); - EXPECT_EQ(output.transferredNumberOfShares, 2); - EXPECT_EQ(output.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.managedShares(asset, owner), 2); - EXPECT_EQ(nostromo.sharesManagedBy(asset, owner, QX_CONTRACT_INDEX), 2); - } - - { - ContractTestingNOST nostromo; - const id owner(9, 10, 11, 12); - const uint64 assetName = assetNameFromString("TRFINS"); - const Asset asset{owner, assetName}; - - EXPECT_EQ(nostromo.issueAsset(owner, assetName, 4), 4); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(owner, asset, 4), 4); - nostromo.syncCachedQxTransferFee(); - - const auto output = nostromo.transferManagedSharesWithReward(owner, asset, 2, QX_CONTRACT_INDEX, nostromo.getCachedQxTransferFee() - 1); - EXPECT_EQ(output.transferredNumberOfShares, 0); - EXPECT_EQ(output.errorCode, NOST::EAuctionError::InvalidInput); - EXPECT_EQ(nostromo.managedShares(asset, owner), 4); - EXPECT_EQ(nostromo.sharesManagedBy(asset, owner, QX_CONTRACT_INDEX), 0); - } - - { - ContractTestingNOST nostromo; - const id owner(13, 14, 15, 16); - const uint64 assetName = assetNameFromString("TRFEXC"); - const Asset asset{owner, assetName}; - - EXPECT_EQ(nostromo.issueAsset(owner, assetName, 4), 4); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(owner, asset, 4), 4); - nostromo.syncCachedQxTransferFee(); - const sint64 reward = static_cast(nostromo.getCachedQxTransferFee()) + 50; - nostromo.seedUser(owner, reward); - const sint64 ownerBefore = getBalance(owner); - - const auto output = nostromo.transferManagedSharesWithFundedReward(owner, asset, 2, QX_CONTRACT_INDEX, reward); - EXPECT_EQ(output.transferredNumberOfShares, 2); - EXPECT_EQ(output.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(getBalance(owner) - ownerBefore, -static_cast(nostromo.getCachedQxTransferFee())); - EXPECT_EQ(nostromo.managedShares(asset, owner), 2); - EXPECT_EQ(nostromo.sharesManagedBy(asset, owner, QX_CONTRACT_INDEX), 2); - } - - { - ContractTestingNOST nostromo; - const id owner(17, 18, 19, 20); - const uint64 assetName = assetNameFromString("TRFINV"); - const Asset asset{owner, assetName}; - - EXPECT_EQ(nostromo.issueAsset(owner, assetName, 4), 4); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(owner, asset, 4), 4); - nostromo.syncCachedQxTransferFee(); - - const auto invalidDestination = nostromo.transferManagedSharesWithReward(owner, asset, 2, 0, nostromo.getCachedQxTransferFee()); - EXPECT_EQ(invalidDestination.transferredNumberOfShares, 0); - EXPECT_EQ(invalidDestination.errorCode, NOST::EAuctionError::InvalidInput); - EXPECT_EQ(nostromo.managedShares(asset, owner), 4); - - Asset zeroAsset{}; - const auto zeroAssetOutput = - nostromo.transferManagedSharesWithReward(owner, zeroAsset, 2, QX_CONTRACT_INDEX, nostromo.getCachedQxTransferFee()); - EXPECT_EQ(zeroAssetOutput.transferredNumberOfShares, 0); - EXPECT_EQ(zeroAssetOutput.errorCode, NOST::EAuctionError::InvalidInput); - EXPECT_EQ(nostromo.managedShares(asset, owner), 4); - } -} -TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) -{ - ContractTestingNOST nostromo; - const id seller(11, 12, 13, 14); - const uint64 assetName = assetNameFromString("CRTBTN"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 9), 9); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 9), 9); - - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 9, 25); - const auto output = nostromo.createAuction(seller, input); - ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(output.auctionIndex, 0ULL); - - const auto auction = nostromo.getAuction(output.auctionIndex).auction; - EXPECT_EQ(auction.core.auctionIndex, output.auctionIndex); - EXPECT_EQ(auction.core.quantityForSale, 9ULL); - EXPECT_EQ(auction.core.minimumPurchaseQuantity, 1ULL); - EXPECT_EQ(auction.core.salePrice, 25ULL); - EXPECT_EQ(auction.core.auctionDurationSeconds, NOST_SECONDS_PER_DAY); - EXPECT_EQ(auction.core.seller, seller); - EXPECT_EQ(auction.core.type, NOST::EAuctionType::Batch); - EXPECT_EQ(auction.core.visibility, NOST::EAuctionVisibility::Public); - EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Active); - EXPECT_EQ(auction.core.auctionLotItems.get(0).asset, asset); - EXPECT_EQ(auction.core.auctionLotItems.get(0).quantity, 9); - EXPECT_EQ(auction.core.metadataIpfsCid.get(0), 'b'); - EXPECT_EQ(nostromo.managedShares(asset, seller), 0); - EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 9); -} - -TEST(ContractNostromoAuction, PublicAuctionCreationAccumulatesConfiguredFeeAndRefundsExcessAuction) -{ - ContractTestingNOST nostromo; - const id seller(901, 902, 903, 904); - const Asset asset{seller, assetNameFromString("BCRFEE")}; - constexpr sint64 configuredFee = 73; - const auto feeInput = nostromo.makeCoordinatorFeeInput(configuredFee); - ASSERT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), feeInput).errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, configuredFee); - - EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 3), 3); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); - const auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 1, 1); - nostromo.seedUser(seller, 1000); - const sint64 sellerBefore = getBalance(seller); - const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - uint64 expectedPool = nostromo.getPendingServiceFeePool().pendingServiceFeePool; - - const auto insufficient = nostromo.createAuctionWithFundedReward(seller, input, configuredFee - 1); - EXPECT_EQ(insufficient.errorCode, NOST::EAuctionError::InsufficientFunds); - EXPECT_EQ(getBalance(seller), sellerBefore); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); - - const auto exact = nostromo.createAuctionWithFundedReward(seller, input, configuredFee); - ASSERT_EQ(exact.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(getBalance(seller), sellerBefore - configuredFee); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + configuredFee); - expectedPool += static_cast(configuredFee); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); - - constexpr sint64 excessReward = configuredFee + 37; - const auto excess = nostromo.createAuctionWithFundedReward(seller, input, excessReward); - ASSERT_EQ(excess.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(getBalance(seller), sellerBefore - 2 * configuredFee); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + 2 * configuredFee); - expectedPool += static_cast(configuredFee); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); - - constexpr sint64 managementConfiguredFee = 29; - const auto managementFeeInput = nostromo.makeManagementFeeInput(managementConfiguredFee); - ASSERT_EQ(nostromo.setAuctionFeesByManagement(ContractTestingNOST::managementWallet(), managementFeeInput).errorCode, - NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, managementConfiguredFee); - const auto managementConfigured = nostromo.createAuctionWithFundedReward(seller, input, managementConfiguredFee); - ASSERT_EQ(managementConfigured.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(getBalance(seller), sellerBefore - (2 * configuredFee + managementConfiguredFee)); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + 2 * configuredFee + managementConfiguredFee); - expectedPool += static_cast(managementConfiguredFee); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); - - const id standardSeller(921, 922, 923, 924); - const Asset standardAsset{standardSeller, assetNameFromString("BCFSTD")}; - ASSERT_EQ(nostromo.issueAsset(standardSeller, standardAsset.assetName, 3), 3); - ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(standardSeller, standardAsset, 3), 3); - const auto standardInput = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(standardAsset, 1)); - nostromo.seedUser(standardSeller, 1000); - const sint64 standardSellerBefore = getBalance(standardSeller); - const sint64 standardContractBefore = getBalance(NOST_CONTRACT_ID); - - const auto standardInsufficient = nostromo.createAuctionWithFundedReward(standardSeller, standardInput, managementConfiguredFee - 1); - EXPECT_EQ(standardInsufficient.errorCode, NOST::EAuctionError::InsufficientFunds); - EXPECT_EQ(getBalance(standardSeller), standardSellerBefore); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID), standardContractBefore); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); - - const auto standardExact = nostromo.createAuctionWithFundedReward(standardSeller, standardInput, managementConfiguredFee); - ASSERT_EQ(standardExact.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(getBalance(standardSeller), standardSellerBefore - managementConfiguredFee); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID), standardContractBefore + managementConfiguredFee); - expectedPool += static_cast(managementConfiguredFee); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); - - constexpr sint64 standardExcessReward = managementConfiguredFee + 17; - const auto standardExcess = nostromo.createAuctionWithFundedReward(standardSeller, standardInput, standardExcessReward); - ASSERT_EQ(standardExcess.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(getBalance(standardSeller), standardSellerBefore - 2 * managementConfiguredFee); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID), standardContractBefore + 2 * managementConfiguredFee); - expectedPool += static_cast(managementConfiguredFee); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); - - const id privateSeller(925, 926, 927, 928); - const id allowedBidder(929, 930, 931, 932); - const Asset privateAsset{privateSeller, assetNameFromString("BCFPRV")}; - ASSERT_EQ(nostromo.issueAsset(privateSeller, privateAsset.assetName, 1), 1); - ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(privateSeller, privateAsset, 1), 1); - auto privateInput = ContractTestingNOST::makeBatchAuctionInput(privateAsset, 1, 1); - privateInput.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - privateInput.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); - nostromo.seedUser(privateSeller, NOST_DEFAULT_PRIVATE_AUCTION_FEE + 100); - const sint64 privateSellerBefore = getBalance(privateSeller); - EXPECT_EQ(nostromo.createAuctionWithFundedReward(privateSeller, privateInput, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, - NOST::EAuctionError::Success); - EXPECT_EQ(getBalance(privateSeller), privateSellerBefore - NOST_DEFAULT_PRIVATE_AUCTION_FEE); - expectedPool += static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); - - const id privateStandardSeller(933, 934, 935, 936); - const Asset privateStandardAsset{privateStandardSeller, assetNameFromString("PRVSTD")}; - ASSERT_EQ(nostromo.issueAsset(privateStandardSeller, privateStandardAsset.assetName, 1), 1); - ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(privateStandardSeller, privateStandardAsset, 1), 1); - auto privateStandardInput = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(privateStandardAsset, 1)); - privateStandardInput.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - privateStandardInput.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); - nostromo.seedUser(privateStandardSeller, NOST_DEFAULT_PRIVATE_AUCTION_FEE + 100); - const sint64 privateStandardSellerBefore = getBalance(privateStandardSeller); - EXPECT_EQ(nostromo.createAuctionWithFundedReward(privateStandardSeller, privateStandardInput, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, - NOST::EAuctionError::Success); - EXPECT_EQ(getBalance(privateStandardSeller), privateStandardSellerBefore - NOST_DEFAULT_PRIVATE_AUCTION_FEE); - expectedPool += static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); - - nostromo.endEpoch(); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, 0ULL); -} - -TEST(ContractNostromoAuction, BatchBidFeeBoundariesAuction) -{ - ContractTestingNOST nostromo; - const struct - { - uint64 bidQuantity; - uint64 bidAmount; - uint64 escrowAmount; - uint64 fee; - uint64 requiredReward; - } cases[] = { - {1, 9, 9, 91, 100}, {1, 10, 10, 90, 100}, {1, 20, 20, 80, 100}, {1, 30, 30, 70, 100}, {10, 100, 1000, 0, 1000}, - {1, 101, 101, 0, 101}, {2, 101, 202, 0, 202}, {2, 19, 38, 62, 100}, {2, 100, 200, 0, 200}, {UINT64_MAX, 2, UINT64_MAX, 0, UINT64_MAX}, - }; - - for (const auto& testCase : cases) - { - SCOPED_TRACE(::testing::Message() << "quantity=" << testCase.bidQuantity << ", bidAmount=" << testCase.bidAmount); - const auto output = nostromo.calculateBatchAuctionBidFee(testCase.bidQuantity, testCase.bidAmount); - EXPECT_EQ(output.escrowAmount, testCase.escrowAmount); - EXPECT_EQ(output.fee, testCase.fee); - EXPECT_EQ(output.requiredReward, testCase.requiredReward); - } -} - -TEST(ContractNostromoAuction, PublicAuctionCreationFeeConfigurationBoundariesAuction) -{ - ContractTestingNOST nostromo; - EXPECT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, NOST_PUBLIC_AUCTION_CREATION_FEE); - - auto coordinatorInput = nostromo.makeCoordinatorFeeInput(0); - ASSERT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput).errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, 0LL); - const id zeroFeeSeller(941, 942, 943, 944); - const Asset zeroFeeAsset{zeroFeeSeller, assetNameFromString("ZEROFEE")}; - ASSERT_EQ(nostromo.issueAsset(zeroFeeSeller, zeroFeeAsset.assetName, 1), 1); - ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(zeroFeeSeller, zeroFeeAsset, 1), 1); - EXPECT_EQ(nostromo.createAuctionWithFundedReward(zeroFeeSeller, ContractTestingNOST::makeBatchAuctionInput(zeroFeeAsset, 1, 1), 0).errorCode, - NOST::EAuctionError::Success); - - coordinatorInput.publicAuctionCreationFee = INT64_MAX; - ASSERT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput).errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, INT64_MAX); - - const auto feesBeforeInvalidUpdate = nostromo.getAuctionFees(); - coordinatorInput.publicAuctionCreationFee = -1; - coordinatorInput.auctionCancellationFeeBasisPoints = 0; - EXPECT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput).errorCode, - NOST::EAuctionError::InvalidInput); - const auto feesAfterInvalidUpdate = nostromo.getAuctionFees(); - EXPECT_EQ(feesAfterInvalidUpdate.publicAuctionCreationFee, feesBeforeInvalidUpdate.publicAuctionCreationFee); - EXPECT_EQ(feesAfterInvalidUpdate.auctionCancellationFeeBasisPoints, feesBeforeInvalidUpdate.auctionCancellationFeeBasisPoints); - - auto managementInput = nostromo.makeManagementFeeInput(41); - ASSERT_EQ(nostromo.setAuctionFeesByManagement(ContractTestingNOST::managementWallet(), managementInput).errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, 41LL); -} - -TEST(ContractNostromoAuction, AcceptedBatchBidAccumulatesFeeAndKeepsEscrowAuction) -{ - ContractTestingNOST nostromo; - const id seller(905, 906, 907, 908); - const id firstBidder(909, 910, 911, 912); - const Asset asset{seller, assetNameFromString("BBDFEE")}; - - EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); - const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 1)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - nostromo.seedUser(firstBidder, 1000); - const sint64 firstBidderBefore = getBalance(firstBidder); - const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - const uint64 poolBefore = nostromo.getPendingServiceFeePool().pendingServiceFeePool; - const auto underfundedSmallBid = nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 1, 9, 99); - EXPECT_EQ(underfundedSmallBid.errorCode, NOST::EAuctionError::InsufficientFunds); - EXPECT_EQ(underfundedSmallBid.refundedAmount, 99ULL); - EXPECT_EQ(getBalance(firstBidder), firstBidderBefore); - - const auto calculation = nostromo.calculateBatchAuctionBidFee(2, 40); - ASSERT_EQ(calculation.escrowAmount, 80ULL); - ASSERT_EQ(calculation.fee, 20ULL); - ASSERT_EQ(calculation.requiredReward, 100ULL); - const auto underfunded = nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 2, 40, calculation.requiredReward - 1); - EXPECT_EQ(underfunded.errorCode, NOST::EAuctionError::InsufficientFunds); - EXPECT_EQ(underfunded.refundedAmount, 99ULL); - EXPECT_EQ(getBalance(firstBidder), firstBidderBefore); - - const auto accepted = nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 2, 40, calculation.requiredReward + 49); - ASSERT_EQ(accepted.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(accepted.escrowedAmount, 80ULL); - EXPECT_EQ(accepted.refundedAmount, 49ULL); - EXPECT_EQ(getBalance(firstBidder), firstBidderBefore - 100); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + 100); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, poolBefore + 20ULL); - EXPECT_EQ(nostromo.getNostromoFeePool().feePool.developmentAmount, poolBefore + 20ULL); - - // The contract defaults to routing every fee to development, so the whole accumulated pool (including the earlier creation fee - // already reflected in contractBefore) leaves the contract at END_EPOCH, leaving only the escrowed amount behind. - ASSERT_EQ(nostromo.getRouteAllFeesToDevelopment(), NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT); - nostromo.endEpoch(); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, 0ULL); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + 80 - static_cast(poolBefore)); -} - -TEST(ContractNostromoAuction, CreateStandardSingleAssetAuctionEscrowsLotAuction) -{ - ContractTestingNOST nostromo; - const id seller(21, 22, 23, 24); - const uint64 assetName = assetNameFromString("CRTSTA"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 5), 5); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 5), 5); - - auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 5)); - input.minimumPurchaseQuantity = UINT64_MAX; - - nostromo.seedUser(seller, 1000); - const sint64 sellerBalanceBefore = getBalance(seller); - const sint64 contractBalanceBefore = getBalance(NOST_CONTRACT_ID); - const uint64 poolBefore = nostromo.getPendingServiceFeePool().pendingServiceFeePool; - const auto output = nostromo.createAuctionWithFundedReward(seller, input, NOST_PUBLIC_AUCTION_CREATION_FEE); - ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(getBalance(seller), sellerBalanceBefore - NOST_PUBLIC_AUCTION_CREATION_FEE); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBalanceBefore + NOST_PUBLIC_AUCTION_CREATION_FEE); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, poolBefore + static_cast(NOST_PUBLIC_AUCTION_CREATION_FEE)); - - const auto auction = nostromo.getAuction(output.auctionIndex).auction; - EXPECT_EQ(auction.core.quantityForSale, 1ULL); - EXPECT_EQ(auction.core.minimumPurchaseQuantity, 0ULL); - EXPECT_EQ(auction.core.initialPrice, NOST_STANDARD_MIN_PRICE); - EXPECT_EQ(auction.core.salePrice, NOST_STANDARD_MIN_PRICE); - EXPECT_EQ(auction.core.minimumBidIncrement, NOST_STANDARD_MIN_BID_INCREMENT); - EXPECT_EQ(auction.core.type, NOST::EAuctionType::Standard); - EXPECT_EQ(auction.core.auctionLotItems.get(0).asset, asset); - EXPECT_EQ(auction.core.auctionLotItems.get(0).quantity, 5); - EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 5); -} - -TEST(ContractNostromoAuction, BatchMinimumPurchaseQuantityCreationBoundsAuction) -{ - ContractTestingNOST nostromo; - const id seller(301, 302, 303, 304); - const Asset asset{seller, assetNameFromString("BATMIN")}; - - EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 30), 30); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 30), 30); - - auto zeroMinimum = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 10); - zeroMinimum.minimumPurchaseQuantity = 0; - EXPECT_EQ(nostromo.createAuction(seller, zeroMinimum).errorCode, NOST::EAuctionError::InvalidInput); - - auto excessiveMinimum = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 10); - excessiveMinimum.minimumPurchaseQuantity = 11; - EXPECT_EQ(nostromo.createAuction(seller, excessiveMinimum).errorCode, NOST::EAuctionError::InvalidInput); - - auto minimumOne = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 10); - const auto minimumOneOutput = nostromo.createAuction(seller, minimumOne); - ASSERT_EQ(minimumOneOutput.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.getAuction(minimumOneOutput.auctionIndex).auction.core.minimumPurchaseQuantity, 1ULL); - - auto fullLotMinimum = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 10); - fullLotMinimum.minimumPurchaseQuantity = 10; - const auto fullLotMinimumOutput = nostromo.createAuction(seller, fullLotMinimum); - ASSERT_EQ(fullLotMinimumOutput.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.getAuction(fullLotMinimumOutput.auctionIndex).auction.core.minimumPurchaseQuantity, 10ULL); -} - -TEST(ContractNostromoAuction, BatchBidEnforcesMinimumPurchaseQuantityAndRefundsAuction) -{ - ContractTestingNOST nostromo; - const id seller(305, 306, 307, 308); - const id bidder(309, 310, 311, 312); - const Asset asset{seller, assetNameFromString("BATBIDM")}; - - EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 15), 15); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 15), 15); - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 15, 10); - input.minimumPurchaseQuantity = 10; - const auto createOutput = nostromo.createAuction(seller, input); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - nostromo.seedUser(bidder, 200); - const sint64 balanceBeforeRejectedBid = getBalance(bidder); - const auto rejectedBid = nostromo.placeBidWithFundedReward(bidder, createOutput.auctionIndex, 9, 10, 90); - EXPECT_EQ(rejectedBid.errorCode, NOST::EAuctionError::InvalidInput); - EXPECT_EQ(rejectedBid.refundedAmount, 90ULL); - EXPECT_EQ(rejectedBid.escrowedAmount, 0ULL); - EXPECT_EQ(getBalance(bidder), balanceBeforeRejectedBid); - - const auto acceptedBid = nostromo.placeBatchBidWithFundedRequiredReward(bidder, createOutput.auctionIndex, 10, 10); - EXPECT_EQ(acceptedBid.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(acceptedBid.escrowedAmount, 100ULL); -} - -TEST(ContractNostromoAuction, CreateStandardAuctionSupportsFourLotEntriesAuction) -{ - ContractTestingNOST nostromo; - const id seller(25, 26, 27, 28); - const id bidder(29, 30, 31, 32); - const Asset assets[] = { - {seller, assetNameFromString("MAXLOA")}, - {seller, assetNameFromString("MAXLOB")}, - {seller, assetNameFromString("MAXLOC")}, - {seller, assetNameFromString("MAXLOD")}, - }; - - EXPECT_EQ(NOST_BATCH_AUCTION_LOT_ITEM_NUM, 1ULL); - EXPECT_EQ(NOST_AUCTION_LOT_ITEM_NUM, 4); - for (const auto& asset : assets) - { - EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 3), 3); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); - } - - const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeLot( - {{assets[0], 3}, {assets[1], 3}, {assets[2], 3}, {assets[3], 3}}))); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - for (const auto& asset : assets) - { - EXPECT_EQ(nostromo.managedShares(asset, seller), 0); - EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 3); - } - - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, - NOST::EAuctionError::Success); - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - - const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; - EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); - for (const auto& asset : assets) - { - EXPECT_EQ(nostromo.managedShares(asset, bidder), 3); - EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 0); - } -} - -TEST(ContractNostromoAuction, CreateBatchAuctionRejectsMultipleLotEntriesAuction) -{ - ContractTestingNOST nostromo; - const id seller(33, 34, 35, 36); - const Asset firstAsset{seller, assetNameFromString("BATLOA")}; - const Asset secondAsset{seller, assetNameFromString("BATLOB")}; - - EXPECT_EQ(nostromo.issueAsset(seller, firstAsset.assetName, 2), 2); - EXPECT_EQ(nostromo.issueAsset(seller, secondAsset.assetName, 2), 2); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, firstAsset, 2), 2); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, secondAsset, 2), 2); - - auto input = ContractTestingNOST::makeBatchAuctionInput(firstAsset, 2); - input.auctionLotItems = ContractTestingNOST::makeLot({{firstAsset, 2}, {secondAsset, 2}}); - - EXPECT_EQ(nostromo.createAuction(seller, input).errorCode, NOST::EAuctionError::InvalidInput); - EXPECT_EQ(nostromo.managedShares(firstAsset, seller), 2); - EXPECT_EQ(nostromo.managedShares(secondAsset, seller), 2); -} - -TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction) -{ - { - ContractTestingNOST nostromo; - const id seller(31, 32, 33, 34); - const id allowedBidder(35, 36, 37, 38); - const uint64 assetName = assetNameFromString("PRIWAL"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 4), 4); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 4), 4); - - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 4, 12); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); - - const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); - - const auto auction = nostromo.getAuction(output.auctionIndex).auction; - EXPECT_EQ(auction.core.visibility, NOST::EAuctionVisibility::Private); - EXPECT_EQ(auction.allowedBidderWalletCount, 1U); - EXPECT_EQ(auction.allowedBidderWallets.get(0), allowedBidder); - EXPECT_EQ(auction.requiredAccessAssetCount, 0U); - } - - { - ContractTestingNOST nostromo; - const id seller(41, 42, 43, 44); - const id gatedBidder(45, 46, 47, 48); - const uint64 saleAssetName = assetNameFromString("PRIACC"); - const uint64 gateAssetName = assetNameFromString("GATEAS"); - const Asset saleAsset{seller, saleAssetName}; - const Asset gateAsset{gatedBidder, gateAssetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, saleAssetName, 5), 5); - EXPECT_EQ(nostromo.issueAsset(gatedBidder, gateAssetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, saleAsset, 5), 5); - - auto input = ContractTestingNOST::makeBatchAuctionInput(saleAsset, 5, 20); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{gateAsset, 1}}); - - const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); - - const auto auction = nostromo.getAuction(output.auctionIndex).auction; - EXPECT_EQ(auction.core.visibility, NOST::EAuctionVisibility::Private); - EXPECT_EQ(auction.allowedBidderWalletCount, 0U); - EXPECT_EQ(auction.requiredAccessAssetCount, 1U); - EXPECT_EQ(auction.requiredAccessAssets.get(0).asset, gateAsset); - EXPECT_EQ(auction.requiredAccessAssets.get(0).quantity, 1); - EXPECT_GT(nostromo.plainShares(gateAsset, gatedBidder), 0); - } -} - -TEST(ContractNostromoAuction, GetAuctionViewExposesAccessListsAndFoundFlagAuction) -{ - { - ContractTestingNOST nostromo; - const id seller(45, 46, 47, 48); - const id walletA(49, 50, 51, 52); - const id walletB(53, 54, 55, 56); - const uint64 assetName = assetNameFromString("VIEWWL"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); - - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({walletA, walletB}); - - const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - const auto auctionOutput = nostromo.getAuction(createOutput.auctionIndex); - EXPECT_EQ(auctionOutput.found, 1); - EXPECT_EQ(auctionOutput.auction.core.auctionIndex, createOutput.auctionIndex); - EXPECT_EQ(auctionOutput.auction.allowedBidderWalletCount, 2U); - EXPECT_TRUE(containsWallet(auctionOutput.auction.allowedBidderWallets, auctionOutput.auction.allowedBidderWalletCount, walletA)); - EXPECT_TRUE(containsWallet(auctionOutput.auction.allowedBidderWallets, auctionOutput.auction.allowedBidderWalletCount, walletB)); - EXPECT_EQ(auctionOutput.auction.requiredAccessAssetCount, 0U); - } - - { - ContractTestingNOST nostromo; - const id seller(57, 58, 59, 60); - const id gateIssuerA(61, 62, 63, 64); - const id gateIssuerB(65, 66, 67, 68); - const uint64 assetName = assetNameFromString("VIEWAC"); - const Asset asset{seller, assetName}; - const Asset accessAssetA{gateIssuerA, assetNameFromString("GATEA1")}; - const Asset accessAssetB{gateIssuerB, assetNameFromString("GATEB1")}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); - - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.requiredAccessAssets = - ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAssetA, 2}, NOST::AuctionAssetEntry{accessAssetB, 5}}); - - const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - const auto auctionOutput = nostromo.getAuction(createOutput.auctionIndex); - EXPECT_EQ(auctionOutput.found, 1); - EXPECT_EQ(auctionOutput.auction.requiredAccessAssetCount, 2U); - EXPECT_TRUE(containsAccessAsset(auctionOutput.auction.requiredAccessAssets, auctionOutput.auction.requiredAccessAssetCount, - NOST::AuctionAssetEntry{accessAssetA, 2})); - EXPECT_TRUE(containsAccessAsset(auctionOutput.auction.requiredAccessAssets, auctionOutput.auction.requiredAccessAssetCount, - NOST::AuctionAssetEntry{accessAssetB, 5})); - EXPECT_EQ(auctionOutput.auction.allowedBidderWalletCount, 0U); - } - - { - ContractTestingNOST nostromo; - const uint64 missingAuction = 999; - const auto auctionOutput = nostromo.getAuction(missingAuction); - EXPECT_EQ(auctionOutput.found, 0); - EXPECT_EQ(auctionOutput.auction.core.auctionIndex, 0ULL); - } -} - -TEST(ContractNostromoAuction, GetAuctionViewDeduplicatesPrivateAccessInputsAuction) -{ - { - ContractTestingNOST nostromo; - const id seller(69, 70, 71, 72); - const id wallet(73, 74, 75, 76); - const uint64 assetName = assetNameFromString("DUPWAL"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); - - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({wallet, wallet}); - - const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; - EXPECT_EQ(auction.allowedBidderWalletCount, 1U); - EXPECT_TRUE(containsWallet(auction.allowedBidderWallets, auction.allowedBidderWalletCount, wallet)); - } - - { - ContractTestingNOST nostromo; - const id seller(77, 78, 79, 80); - const id gateIssuer(81, 82, 83, 84); - const uint64 assetName = assetNameFromString("DUPACC"); - const Asset asset{seller, assetName}; - const Asset accessAsset{gateIssuer, assetNameFromString("GATEDP")}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); - - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets( - {NOST::AuctionAssetEntry{accessAsset, 2}, NOST::AuctionAssetEntry{accessAsset, 5}, NOST::AuctionAssetEntry{accessAsset, 3}}); - - const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; - EXPECT_EQ(auction.requiredAccessAssetCount, 1U); - EXPECT_TRUE(containsAccessAsset(auction.requiredAccessAssets, auction.requiredAccessAssetCount, NOST::AuctionAssetEntry{accessAsset, 5})); - } -} - -TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacityAuction) -{ - { - ContractTestingNOST nostromo; - const id seller(47, 48, 49, 50); - const id allowedBidder(30007, 31007, 32007, 33007); - const uint64 assetName = assetNameFromString("MAXWAL"); - const Asset asset{seller, assetName}; - Array allowedWallets{}; - EXPECT_EQ(NOST_AUCTION_ALLOWED_WALLET_NUM, 16ULL); - - for (uint64 index = 0; index < NOST_AUCTION_ALLOWED_WALLET_NUM; ++index) - { - allowedWallets.set(index, id(30000 + index, 31000 + index, 32000 + index, 33000 + index)); - } - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.allowedBidderWallets = allowedWallets; - const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - const auto auctionOutput = nostromo.getAuction(createOutput.auctionIndex); - EXPECT_EQ(auctionOutput.auction.allowedBidderWalletCount, NOST_AUCTION_ALLOWED_WALLET_NUM); - EXPECT_TRUE(containsWallet(auctionOutput.auction.allowedBidderWallets, auctionOutput.auction.allowedBidderWalletCount, allowedBidder)); - EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(allowedBidder, createOutput.auctionIndex, 1, 10).errorCode, NOST::EAuctionError::Success); - } - - { - ContractTestingNOST nostromo; - const id seller(57, 58, 59, 60); - const id accessBidder(61, 62, 63, 64); - const id gateIssuer(65, 66, 67, 68); - const uint64 saleAssetName = assetNameFromString("MAXACC"); - const uint64 bidderAccessAssetName = assetNameFromString("MAXACB"); - const Asset saleAsset{seller, saleAssetName}; - Array requiredAssets{}; - - for (uint64 index = 0; index < NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM; ++index) - { - requiredAssets.set(index, NOST::AuctionAssetEntry{Asset{gateIssuer, 34000 + index}, 1}); - } - requiredAssets.set(NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM - 1, NOST::AuctionAssetEntry{Asset{accessBidder, bidderAccessAssetName}, 1}); - - EXPECT_EQ(nostromo.issueAsset(seller, saleAssetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, saleAsset, 1), 1); - EXPECT_EQ(nostromo.issueAsset(accessBidder, bidderAccessAssetName, 1), 1); - - auto input = ContractTestingNOST::makeBatchAuctionInput(saleAsset, 1, 10); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.requiredAccessAssets = requiredAssets; - const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - const auto auctionOutput = nostromo.getAuction(createOutput.auctionIndex); - EXPECT_EQ(auctionOutput.auction.requiredAccessAssetCount, NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM); - EXPECT_TRUE(containsAccessAsset(auctionOutput.auction.requiredAccessAssets, auctionOutput.auction.requiredAccessAssetCount, - NOST::AuctionAssetEntry{Asset{accessBidder, bidderAccessAssetName}, 1})); - EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(accessBidder, createOutput.auctionIndex, 1, 10).errorCode, NOST::EAuctionError::Success); - } -} - -TEST(ContractNostromoAuction, PrivateAuctionFeeIsDistributedAcrossRecipientsAuction) -{ - const uint8 routeModes[] = {0, 1}; - for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) - { - SCOPED_TRACE(::testing::Message() << "routeAllFeesToDevelopment=" << static_cast(routeModes[routeIndex])); - ContractTestingNOST nostromo; - const uint8 routeMode = routeModes[routeIndex]; - const id seller(61 + routeIndex, 62 + routeIndex, 63 + routeIndex, 64 + routeIndex); - const id allowedBidder(71 + routeIndex, 72 + routeIndex, 73 + routeIndex, 74 + routeIndex); - const uint64 assetName = assetNameFromString(routeMode ? "STDENR1" : "STDENR0"); - - const Asset asset{seller, assetName}; - - nostromo.setRouteAllFeesToDevelopment(routeMode); - EXPECT_EQ(nostromo.getRouteAllFeesToDevelopment(), routeMode); - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 4), 4); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 4), 4); - - const sint64 sellerBefore = getBalance(seller); - const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); - const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); - const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); - const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - constexpr uint64 expectedShareholderDividend = 36350000ULL; - constexpr uint64 expectedManagementFee = 4550000ULL; - constexpr uint64 expectedDevelopmentFee = 4550000ULL; - constexpr uint64 expectedCoordinatorFee = 4550000ULL; - const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedShareholderDividend); - - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 4, 12); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); - - const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); - - EXPECT_EQ(getBalance(seller) - sellerBefore, 0); - - // The fee accumulates in the pool and is not distributed until END_EPOCH, regardless of the route-to-development mode. - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE)); - const auto pendingFeePool = nostromo.getNostromoFeePool(); - EXPECT_EQ(pendingFeePool.totalAmount, static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE)); - EXPECT_EQ(pendingFeePool.feePool.commonServiceFeeAmount, routeMode == 0 ? static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE) : 0ULL); - EXPECT_EQ(pendingFeePool.feePool.developmentAmount, routeMode != 0 ? static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE) : 0ULL); - - nostromo.endEpoch(); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, 0ULL); - EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 0ULL); - - if (routeMode != 0) - { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); - } - else - { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedManagementFee); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedDevelopmentFee); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedCoordinatorFee); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); - } - } -} - -TEST(ContractNostromoAuction, EndEpochUsesCurrentManagementWalletForAccruedFeesAuction) -{ - ContractTestingNOST nostromo; - const id seller(1501, 1502, 1503, 1504); - const id allowedBidder(1505, 1506, 1507, 1508); - const id newManagement(1509, 1510, 1511, 1512); - const Asset asset{seller, assetNameFromString("CURMGR")}; - - nostromo.setRouteAllFeesToDevelopment(0); - ASSERT_EQ(nostromo.issueAsset(seller, asset.assetName, 1), 1); - ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); - ASSERT_EQ(nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, NOST::EAuctionError::Success); - - nostromo.ensureUser(newManagement); - const sint64 previousManagementBefore = getBalance(ContractTestingNOST::managementWallet()); - const sint64 newManagementBefore = getBalance(newManagement); - ASSERT_EQ(nostromo.setManagement(ContractTestingNOST::takeoverCoordinatorWallet(), newManagement).errorCode, NOST::EAuctionError::Success); - - nostromo.endEpoch(); - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()), previousManagementBefore); - EXPECT_EQ(getBalance(newManagement) - newManagementBefore, 4550000ULL); -} - -TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) -{ - ContractTestingNOST nostromo; - const id seller(51, 52, 53, 54); - const id altIssuer(55, 56, 57, 58); - const uint64 assetNameA = assetNameFromString("INVAAA"); - const Asset assetA{seller, assetNameA}; - const Asset accessAsset{altIssuer, assetNameFromString("GATINV")}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetNameA, 5), 5); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetA, 5), 5); - EXPECT_EQ(nostromo.issueAsset(altIssuer, assetNameFromString("GATINV"), 1), 1); - nostromo.seedUser(seller, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - const sint64 sellerBalanceBeforeInvalidCalls = getBalance(seller); - const sint64 contractBalanceBeforeInvalidCalls = getBalance(NOST_CONTRACT_ID); - const auto invokeRejectedPublicAuction = [&nostromo, &seller](const NOST::CreateAuction_input& input) { - return nostromo.createAuctionWithFundedReward(seller, input, NOST_PUBLIC_AUCTION_CREATION_FEE); - }; - const auto invokeRejectedPrivateAuction = [&nostromo, &seller](const NOST::CreateAuction_input& input) { - return nostromo.createAuctionWithFundedReward(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - }; - - auto invalidCid = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - invalidCid.metadataIpfsCid = ContractTestingNOST::makeInvalidMetadataCidFirstChar(); - EXPECT_EQ(invokeRejectedPublicAuction(invalidCid).errorCode, NOST::EAuctionError::InvalidInput); - - auto invalidCidUppercase = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - invalidCidUppercase.metadataIpfsCid = ContractTestingNOST::makeInvalidMetadataCidUppercase(); - EXPECT_EQ(invokeRejectedPublicAuction(invalidCidUppercase).errorCode, NOST::EAuctionError::InvalidInput); - - auto emptyLot = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - emptyLot.auctionLotItems = Array{}; - EXPECT_EQ(invokeRejectedPublicAuction(emptyLot).errorCode, NOST::EAuctionError::InvalidInput); - - auto negativeQuantity = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - negativeQuantity.auctionLotItems = ContractTestingNOST::makeSingleLot(assetA, -1); - EXPECT_EQ(invokeRejectedPublicAuction(negativeQuantity).errorCode, NOST::EAuctionError::InvalidInput); - - auto zeroDuration = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - zeroDuration.durationDays = 0; - EXPECT_EQ(invokeRejectedPublicAuction(zeroDuration).errorCode, NOST::EAuctionError::InvalidInput); - - auto tooLongDuration = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - tooLongDuration.durationDays = NOST_AUCTION_MAX_DURATION_DAYS + 1; - EXPECT_EQ(invokeRejectedPublicAuction(tooLongDuration).errorCode, NOST::EAuctionError::InvalidInput); - - auto invalidType = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - invalidType.auctionType = 99; - EXPECT_EQ(invokeRejectedPublicAuction(invalidType).errorCode, NOST::EAuctionError::InvalidAuctionType); - - auto invalidVisibility = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - invalidVisibility.auctionVisibility = 99; - EXPECT_EQ(invokeRejectedPublicAuction(invalidVisibility).errorCode, NOST::EAuctionError::InvalidVisibility); - - auto partiallyEmptyLot = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - partiallyEmptyLot.auctionLotItems = ContractTestingNOST::makeSingleLot(Asset{}, 1); - EXPECT_EQ(invokeRejectedPublicAuction(partiallyEmptyLot).errorCode, NOST::EAuctionError::InvalidInput); - - auto invalidBatchBuyNow = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - invalidBatchBuyNow.buyNowPrice = 100; - EXPECT_EQ(invokeRejectedPublicAuction(invalidBatchBuyNow).errorCode, NOST::EAuctionError::InvalidInput); - - auto invalidStandardIncrement = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); - invalidStandardIncrement.minimumBidIncrement = 0; - EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardIncrement).errorCode, NOST::EAuctionError::InvalidInput); - - auto invalidStandardLowInitial = ContractTestingNOST::makeStandardAuctionInput( - ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE - 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_BID_INCREMENT); - EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardLowInitial).errorCode, NOST::EAuctionError::InvalidInput); - - auto invalidStandardLowSale = ContractTestingNOST::makeStandardAuctionInput( - ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE - 1, NOST_STANDARD_MIN_BID_INCREMENT); - EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardLowSale).errorCode, NOST::EAuctionError::InvalidInput); - - auto invalidStandardLowIncrement = ContractTestingNOST::makeStandardAuctionInput( - ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_BID_INCREMENT - 1); - EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardLowIncrement).errorCode, NOST::EAuctionError::InvalidInput); - - auto invalidStandardPrice = ContractTestingNOST::makeStandardAuctionInput( - ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE + 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_BID_INCREMENT); - EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardPrice).errorCode, NOST::EAuctionError::InvalidInput); - - auto invalidStandardSalePrice = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); - invalidStandardSalePrice.salePrice = 0; - EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardSalePrice).errorCode, NOST::EAuctionError::InvalidInput); - - auto invalidStandardBuyNow = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE, - NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT, - NOST_STANDARD_MIN_BID_INCREMENT, NOST_STANDARD_MIN_PRICE - 1); - EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardBuyNow).errorCode, NOST::EAuctionError::InvalidInput); - - auto privateWithoutGate = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - privateWithoutGate.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - EXPECT_EQ(invokeRejectedPrivateAuction(privateWithoutGate).errorCode, NOST::EAuctionError::InvalidInput); - - auto zeroAccessQuantity = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - zeroAccessQuantity.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - zeroAccessQuantity.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAsset, 0}}); - EXPECT_EQ(invokeRejectedPrivateAuction(zeroAccessQuantity).errorCode, NOST::EAuctionError::InvalidInput); - - auto negativeAccessQuantity = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - negativeAccessQuantity.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - negativeAccessQuantity.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAsset, -1}}); - EXPECT_EQ(invokeRejectedPrivateAuction(negativeAccessQuantity).errorCode, NOST::EAuctionError::InvalidInput); - - auto partiallyEmptyAccessAsset = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - partiallyEmptyAccessAsset.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - partiallyEmptyAccessAsset.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{Asset{}, 1}}); - EXPECT_EQ(invokeRejectedPrivateAuction(partiallyEmptyAccessAsset).errorCode, NOST::EAuctionError::InvalidInput); - - EXPECT_EQ(getBalance(seller), sellerBalanceBeforeInvalidCalls); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBalanceBeforeInvalidCalls); - EXPECT_EQ(nostromo.managedShares(assetA, seller), 5); - EXPECT_EQ(nostromo.getLatestAuctionIndex().found, 0); - EXPECT_EQ(nostromo.getContractStats().stats.totalAuctionsCreated, 0ULL); - EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 0ULL); -} - -TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientAssetBalanceAndPauseAuction) -{ - { - ContractTestingNOST nostromo; - const id seller(61, 62, 63, 64); - const uint64 assetName = assetNameFromString("PRIFEE"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 4), 4); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 4), 4); - - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 4, 10); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({id(1, 1, 1, 1)}); - - const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE - 1); - EXPECT_EQ(output.errorCode, NOST::EAuctionError::InsufficientFunds); - EXPECT_EQ(nostromo.managedShares(asset, seller), 4); - } - - { - ContractTestingNOST nostromo; - const id seller(71, 72, 73, 74); - const uint64 assetName = assetNameFromString("BALLOW"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); - - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 3, 10); - const auto output = nostromo.createAuction(seller, input); - EXPECT_EQ(output.errorCode, NOST::EAuctionError::InsufficientAssetBalance); - EXPECT_EQ(nostromo.managedShares(asset, seller), 2); - } - - { - ContractTestingNOST nostromo; - const id seller(81, 82, 83, 84); - const uint64 assetName = assetNameFromString("PAUSEA"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); - - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 3, 10); - nostromo.setNow(2026, 1, 7, 11, 40, 0); - nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); - - const auto output = nostromo.createAuction(seller, input); - EXPECT_EQ(output.errorCode, NOST::EAuctionError::AuctionPaused); - EXPECT_EQ(output.auctionIndex, 0ULL); - EXPECT_EQ(nostromo.managedShares(asset, seller), 3); - } - - { - ContractTestingNOST nostromo; - const id seller(85, 86, 87, 88); - const uint64 assetName = assetNameFromString("BOOTPA"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); - - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 3, 10); - nostromo.setNow(2022, 4, 13, 12, 0, 0); - nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); - - const auto output = nostromo.createAuction(seller, input); - EXPECT_EQ(output.errorCode, NOST::EAuctionError::AuctionPaused); - EXPECT_EQ(output.auctionIndex, 0ULL); - EXPECT_EQ(nostromo.managedShares(asset, seller), 3); - } -} - -TEST(ContractNostromoAuction, CreateAuctionRejectsWhenAuctionStorageIsFullAuction) -{ - ContractTestingNOST nostromo; - const id seller(87, 88, 89, 90); - const uint64 assetName = assetNameFromString("STOFUL"); - const Asset asset{seller, assetName}; - - for (uint64 index = 0; index < NOST_AUCTION_NUM; ++index) - { - NOST::AuctionData auction{}; - auction.core.auctionIndex = index; - auction.core.seller = seller; - auction.core.status = NOST::EAuctionStatus::Active; - ASSERT_NE(nostromo.stateData().auctionList.set(auction.core.auctionIndex, auction), NULL_INDEX); - } - ASSERT_EQ(nostromo.stateData().auctionList.population(), NOST_AUCTION_NUM); - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - - const auto output = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10)); - EXPECT_EQ(output.errorCode, NOST::EAuctionError::StorageFull); - EXPECT_EQ(output.auctionIndex, 0ULL); - EXPECT_EQ(nostromo.managedShares(asset, seller), 1); -} - -TEST(ContractNostromoAuction, CreateAuctionRejectsWhenAuctionIndexIsExhaustedAuction) -{ - ContractTestingNOST nostromo; - const id seller(89, 90, 91, 92); - const uint64 assetName = assetNameFromString("IDXMAX"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - nostromo.stateData().totalAuctionsCreated = UINT64_MAX; - - const auto output = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10)); - EXPECT_EQ(output.errorCode, NOST::EAuctionError::AuctionIndexExhausted); - EXPECT_EQ(output.auctionIndex, 0ULL); - EXPECT_EQ(nostromo.stateData().totalAuctionsCreated, UINT64_MAX); - EXPECT_EQ(nostromo.stateData().auctionList.population(), 0ULL); - EXPECT_EQ(nostromo.managedShares(asset, seller), 1); -} - -TEST(ContractNostromoAuction, PlaceBidRejectsWhenParticipantStorageIsFullAuction) -{ - ContractTestingNOST nostromo; - const id seller(91, 92, 93, 94); - const id bidder(95, 96, 97, 98); - const uint64 assetName = assetNameFromString("PARFUL"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - - const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - for (uint64 index = 0; index < NOST_AUCTION_PARTICIPANT_NUM; ++index) - { - NOST::AuctionParticipantData participant{}; - participant.auctionIndex = index + 100000ULL; - participant.bidIndex = index; - participant.participant = id(14000 + index, 15000 + index, 16000 + index, 17000 + index); - participant.bidAmount = 1; - participant.requestedQuantity = 1; - participant.isUsed = 1; - participant.isActive = 1; - nostromo.stateData().participants.set(index, participant); - } - uint64 usedParticipantCount = 0; - for (uint64 index = 0; index < NOST_AUCTION_PARTICIPANT_NUM; ++index) - { - if (nostromo.stateData().participants.get(index).isUsed) - { - ++usedParticipantCount; - } - } - ASSERT_EQ(usedParticipantCount, NOST_AUCTION_PARTICIPANT_NUM); - - const auto output = nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, 1, 10); - EXPECT_EQ(output.errorCode, NOST::EAuctionError::StorageFull); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.highestBidAmount, 0ULL); - EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidder).found, 0); -} - -TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAuction) -{ - ContractTestingNOST nostromo; - const id seller(91, 92, 93, 94); - const id bidderA(95, 96, 97, 98); - const id bidderB(99, 100, 101, 102); - const id bidderC(103, 104, 105, 106); - const uint64 assetName = assetNameFromString("BIDBAT"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 6), 6); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 6), 6); - - const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 6, 10)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - const auto sellerBid = nostromo.placeBid(seller, createOutput.auctionIndex, 1, 12, 12); - EXPECT_EQ(sellerBid.errorCode, NOST::EAuctionError::Forbidden); - - const auto missingAuction = nostromo.placeBid(bidderA, 700, 1, 12, 12); - EXPECT_EQ(missingAuction.errorCode, NOST::EAuctionError::AuctionNotFound); - - const auto zeroQuantity = nostromo.placeBid(bidderA, createOutput.auctionIndex, 0, 12, 12); - EXPECT_EQ(zeroQuantity.errorCode, NOST::EAuctionError::InvalidInput); - - const auto zeroBid = nostromo.placeBid(bidderA, createOutput.auctionIndex, 1, 0, 1); - EXPECT_EQ(zeroBid.errorCode, NOST::EAuctionError::InvalidInput); - - const auto tooLow = nostromo.placeBid(bidderA, createOutput.auctionIndex, 1, 9, 9); - EXPECT_EQ(tooLow.errorCode, NOST::EAuctionError::BidTooLow); - - const auto insufficientFunds = nostromo.placeBid(bidderA, createOutput.auctionIndex, 2, 12, 23); - EXPECT_EQ(insufficientFunds.errorCode, NOST::EAuctionError::InsufficientFunds); - - const auto bidA1 = nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 2, 20); - const auto bidB = nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 3, 15); - ASSERT_EQ(bidA1.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(bidB.errorCode, NOST::EAuctionError::Success); - - auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; - EXPECT_EQ(auction.core.highestBidder, bidderA); - EXPECT_EQ(auction.core.highestBidPrice, 20ULL); - EXPECT_EQ(auction.core.highestBidAmount, 40ULL); - - const auto bidA2 = nostromo.placeBid(bidderA, createOutput.auctionIndex, 2, 14, 28); - EXPECT_EQ(bidA2.errorCode, NOST::EAuctionError::QuantityUnavailable); - EXPECT_EQ(bidA2.refundedAmount, 28ULL); - - auction = nostromo.getAuction(createOutput.auctionIndex).auction; - EXPECT_EQ(auction.core.highestBidder, bidderA); - EXPECT_EQ(auction.core.highestBidPrice, 20ULL); - EXPECT_EQ(auction.core.highestBidAmount, 40ULL); - - const auto participantA = nostromo.getParticipant(createOutput.auctionIndex, bidderA); - ASSERT_EQ(participantA.found, 1); - EXPECT_EQ(participantA.participantData.escrowedAmount, 40ULL); - EXPECT_EQ(participantA.participantData.bidAmount, 20ULL); - - nostromo.setNow(2026, 1, 2, 9, 0, 1); - const auto closed = nostromo.placeBid(bidderC, createOutput.auctionIndex, 1, 30, 30); - EXPECT_EQ(closed.errorCode, NOST::EAuctionError::AuctionClosed); -} - -TEST(ContractNostromoAuction, PlaceBatchBidExtendsAuctionNearEndAuction) -{ - ContractTestingNOST nostromo; - const id seller(111, 112, 113, 114); - const id bidder(115, 116, 117, 118); - const uint64 assetName = assetNameFromString("BIDEXT"); - const Asset asset{seller, assetName}; +#include +#include - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); - - const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - nostromo.setNow(2026, 1, 2, 8, 56, 30); - const auto bidOutput = nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, 1, 15); - ASSERT_EQ(bidOutput.errorCode, NOST::EAuctionError::Success); - - const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; - EXPECT_EQ(auction.core.auctionDurationSeconds, NOST_SECONDS_PER_DAY + NOST_AUCTION_EXTENSION_SECONDS); -} - -TEST(ContractNostromoAuction, BatchBidAvailabilityRejectsOversizedTailAuction) -{ - ContractTestingNOST nostromo; - const id seller(601, 602, 603, 604); - const id bidderA(605, 606, 607, 608); - const id bidderB(609, 610, 611, 612); - const Asset asset{seller, assetNameFromString("BAVAIL")}; - - EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); - const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 8, 40).errorCode, NOST::EAuctionError::Success); - auto availability = nostromo.getBatchAvailability(createOutput.auctionIndex); - EXPECT_EQ(availability.found, 1); - EXPECT_EQ(availability.isAcceptingBids, 1); - EXPECT_EQ(availability.minimumBidPrice, 2ULL); - EXPECT_EQ(availability.availableQuantity, 2ULL); - - const auto oversized = nostromo.placeBid(bidderB, createOutput.auctionIndex, 3, 20, 6); - EXPECT_EQ(oversized.errorCode, NOST::EAuctionError::QuantityUnavailable); - EXPECT_EQ(oversized.refundedAmount, 6ULL); - EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidderB).found, 0); - - const auto exactTail = nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 2, 20); - EXPECT_EQ(exactTail.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidderB).participantData.requestedQuantity, 2ULL); -} - -TEST(ContractNostromoAuction, BatchCoveredLotRequiresHigherPriceAuction) -{ - ContractTestingNOST nostromo; - const id seller(613, 614, 615, 616); - const id bidderA(617, 618, 619, 620); - const id bidderB(621, 622, 623, 624); - const Asset asset{seller, assetNameFromString("BCOVER")}; - - EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); - const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 10, 30).errorCode, NOST::EAuctionError::Success); - auto availability = nostromo.getBatchAvailability(createOutput.auctionIndex); - EXPECT_EQ(availability.minimumBidPrice, 31ULL); - EXPECT_EQ(availability.availableQuantity, 0ULL); - EXPECT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, 20, 2).errorCode, NOST::EAuctionError::BidTooLow); - EXPECT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, 30, 3).errorCode, NOST::EAuctionError::BidTooLow); - - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 3, 40).errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidderA).participantData.requestedQuantity, 7ULL); - EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidderB).participantData.requestedQuantity, 3ULL); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.managedShares(asset, bidderA), 7); - EXPECT_EQ(nostromo.managedShares(asset, bidderB), 3); - EXPECT_EQ(nostromo.managedShares(asset, seller), 0); -} - -TEST(ContractNostromoAuction, BatchMinimumPurchaseTailAndSameBidderDisplacementAuction) -{ - { - ContractTestingNOST nostromo; - const id seller(625, 626, 627, 628); - const id bidderA(629, 630, 631, 632); - const id bidderB(633, 634, 635, 636); - const Asset asset{seller, assetNameFromString("BTAILM")}; - - EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2); - input.minimumPurchaseQuantity = 3; - const auto createOutput = nostromo.createAuction(seller, input); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 8, 40).errorCode, NOST::EAuctionError::Success); - auto availability = nostromo.getBatchAvailability(createOutput.auctionIndex); - EXPECT_EQ(availability.minimumBidPrice, 41ULL); - EXPECT_EQ(availability.availableQuantity, 0ULL); - EXPECT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 2, 4, 8).errorCode, NOST::EAuctionError::InvalidInput); - EXPECT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 3, 40, 12).errorCode, NOST::EAuctionError::BidTooLow); - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 3, 50).errorCode, NOST::EAuctionError::Success); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.managedShares(asset, bidderA), 7); - EXPECT_EQ(nostromo.managedShares(asset, bidderB), 3); - EXPECT_EQ(nostromo.managedShares(asset, seller), 0); - } - - { - ContractTestingNOST nostromo; - const id seller(637, 638, 639, 640); - const id bidderA(641, 642, 643, 644); - const Asset asset{seller, assetNameFromString("BSAMEB")}; - - EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2); - input.minimumPurchaseQuantity = 3; - const auto createOutput = nostromo.createAuction(seller, input); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 10, 30).errorCode, NOST::EAuctionError::Success); - const auto improved = nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 8, 40); - EXPECT_EQ(improved.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(improved.refundedAmount, 300ULL); - - const auto participants = nostromo.getAuctionParticipants(createOutput.auctionIndex, 0, 64); - ASSERT_EQ(participants.totalCount, 2ULL); - uint64 quantityAtThirty = 0; - uint64 quantityAtForty = 0; - for (uint64 index = 0; index < participants.returnedCount; ++index) - { - if (participants.participants.get(index).bidAmount == 30) - { - quantityAtThirty = participants.participants.get(index).requestedQuantity; - } - if (participants.participants.get(index).bidAmount == 40) - { - quantityAtForty = participants.participants.get(index).requestedQuantity; - } - } - EXPECT_EQ(quantityAtThirty, 0ULL); - EXPECT_EQ(quantityAtForty, 8ULL); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.managedShares(asset, bidderA), 8); - EXPECT_EQ(nostromo.managedShares(asset, seller), 2); - } -} - -TEST(ContractNostromoAuction, BatchDisplacementKeepsExactMinimumResidualAuction) -{ - ContractTestingNOST nostromo; - const id seller(6601, 6602, 6603, 6604); - const id bidderA(6611, 6612, 6613, 6614); - const id bidderB(6621, 6622, 6623, 6624); - const Asset asset{seller, assetNameFromString("BMINEX")}; - - ASSERT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); - ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2); - input.minimumPurchaseQuantity = 3; - const auto createOutput = nostromo.createAuction(seller, input); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 6, 30).errorCode, NOST::EAuctionError::Success); - const auto higherBid = nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 7, 40); - ASSERT_EQ(higherBid.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(higherBid.refundedAmount, 90ULL); - EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidderA).participantData.requestedQuantity, 3ULL); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.managedShares(asset, bidderA), 3); - EXPECT_EQ(nostromo.managedShares(asset, bidderB), 7); - EXPECT_EQ(nostromo.managedShares(asset, seller), 0); -} - -TEST(ContractNostromoAuction, DeterministicBatchAllocationPropertiesAuction) -{ - uint64 generatorState = 0x9E3779B97F4A7C15ULL; - // Unsigned wraparound is intentional: this fixed LCG makes boundary-heavy scenarios reproducible. - for (uint64 scenario = 0; scenario < 12; ++scenario) - { - SCOPED_TRACE(::testing::Message() << "scenario=" << scenario); - ContractTestingNOST nostromo; - generatorState = generatorState * 6364136223846793005ULL + 1442695040888963407ULL; - const uint64 quantityForSale = 3ULL + generatorState % 6ULL; - generatorState = generatorState * 6364136223846793005ULL + 1442695040888963407ULL; - const uint64 minimumPurchaseQuantity = 1ULL + generatorState % quantityForSale; - const id seller(7000 + scenario, 7100 + scenario, 7200 + scenario, 7300 + scenario); - const Asset asset{seller, assetNameFromString("PROPBA")}; - - ASSERT_EQ(nostromo.issueAsset(seller, asset.assetName, static_cast(quantityForSale)), static_cast(quantityForSale)); - ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, static_cast(quantityForSale)), - static_cast(quantityForSale)); - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, static_cast(quantityForSale), 10); - input.minimumPurchaseQuantity = minimumPurchaseQuantity; - const auto createOutput = nostromo.createAuction(seller, input); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - for (uint64 bidIndex = 0; bidIndex < 6; ++bidIndex) - { - SCOPED_TRACE(::testing::Message() << "bidIndex=" << bidIndex); - generatorState = generatorState * 6364136223846793005ULL + 1442695040888963407ULL; - const uint64 bidQuantity = minimumPurchaseQuantity + generatorState % (quantityForSale - minimumPurchaseQuantity + 1ULL); - const uint64 bidPrice = 20ULL + bidIndex * 10ULL; - const id bidder(8000 + scenario * 10 + bidIndex, 9000 + bidIndex, 10000 + scenario, 11000 + bidIndex); - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, bidQuantity, bidPrice).errorCode, - NOST::EAuctionError::Success); - } - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; - const auto participants = nostromo.getAuctionParticipants(createOutput.auctionIndex, 0, NOST_AUCTION_GETTER_PAGE_SIZE); - EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); - uint64 allocatedTotal = 0; - for (uint64 participantIndex = 0; participantIndex < participants.returnedCount; ++participantIndex) - { - const auto participant = participants.participants.get(participantIndex); - EXPECT_LE(participant.allocatedQuantity, participant.requestedQuantity); - EXPECT_TRUE(participant.allocatedQuantity == 0 || participant.allocatedQuantity >= minimumPurchaseQuantity); - allocatedTotal += participant.allocatedQuantity; - } - EXPECT_EQ(allocatedTotal, auction.core.allocatedQuantity); - EXPECT_LE(allocatedTotal, quantityForSale); - EXPECT_EQ(nostromo.managedShares(asset, seller), static_cast(quantityForSale - allocatedTotal)); - } -} - -TEST(ContractNostromoAuction, BatchAvailabilityGetterStatesAuction) -{ - ContractTestingNOST nostromo; - const id batchSeller(645, 646, 647, 648); - const id standardSeller(649, 650, 651, 652); - const id bidder(653, 654, 655, 656); - const Asset closedBatchAsset{batchSeller, assetNameFromString("BGETCL")}; - const Asset maxBatchAsset{batchSeller, assetNameFromString("BGETMX")}; - const Asset standardAsset{standardSeller, assetNameFromString("BGETST")}; - - EXPECT_EQ(nostromo.getBatchAvailability(999).found, 0); - - EXPECT_EQ(nostromo.issueAsset(standardSeller, standardAsset.assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(standardSeller, standardAsset, 1), 1); - const auto standardCreate = - nostromo.createAuction(standardSeller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(standardAsset, 1))); - ASSERT_EQ(standardCreate.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.getBatchAvailability(standardCreate.auctionIndex).found, 1); - EXPECT_EQ(nostromo.getBatchAvailability(standardCreate.auctionIndex).isAcceptingBids, 0); - - EXPECT_EQ(nostromo.issueAsset(batchSeller, closedBatchAsset.assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(batchSeller, closedBatchAsset, 1), 1); - const auto closedBatchCreate = nostromo.createAuction(batchSeller, ContractTestingNOST::makeBatchAuctionInput(closedBatchAsset, 1, 2)); - ASSERT_EQ(closedBatchCreate.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, closedBatchCreate.auctionIndex, 1, 2).errorCode, NOST::EAuctionError::Success); - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.getBatchAvailability(closedBatchCreate.auctionIndex).found, 1); - EXPECT_EQ(nostromo.getBatchAvailability(closedBatchCreate.auctionIndex).isAcceptingBids, 0); - - EXPECT_EQ(nostromo.issueAsset(batchSeller, maxBatchAsset.assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(batchSeller, maxBatchAsset, 1), 1); - const auto batchCreate = nostromo.createAuction(batchSeller, ContractTestingNOST::makeBatchAuctionInput(maxBatchAsset, 1, 2)); - ASSERT_EQ(batchCreate.errorCode, NOST::EAuctionError::Success); - NOST::AuctionParticipantData maxPriceBid{}; - maxPriceBid.auctionIndex = batchCreate.auctionIndex; - maxPriceBid.bidIndex = 0; - maxPriceBid.participant = bidder; - maxPriceBid.bidAmount = UINT64_MAX; - maxPriceBid.requestedQuantity = 1; - maxPriceBid.escrowedAmount = 1; - maxPriceBid.isUsed = 1; - maxPriceBid.isActive = 1; - maxPriceBid.isWinningBid = 1; - nostromo.stateData().participants.set(0, maxPriceBid); - EXPECT_EQ(nostromo.getBatchAvailability(batchCreate.auctionIndex).isAcceptingBids, 0); -} - -TEST(ContractNostromoAuction, PlaceStandardBidValidatesRefundsAndPauseAuction) -{ - ContractTestingNOST nostromo; - const id seller(121, 122, 123, 124); - const id bidderA(125, 126, 127, 128); - const id bidderB(129, 130, 131, 132); - const uint64 assetName = assetNameFromString("STDVAL"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - - const auto createOutput = nostromo.createAuction( - seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, - NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - const auto sellerBid = nostromo.placeBid(seller, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE); - EXPECT_EQ(sellerBid.errorCode, NOST::EAuctionError::Forbidden); - - const auto lowStart = nostromo.placeBid(bidderA, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE - 1, NOST_STANDARD_MIN_PRICE - 1); - EXPECT_EQ(lowStart.errorCode, NOST::EAuctionError::BidTooLow); - - const auto openingBid = nostromo.placeBid(bidderA, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE); - ASSERT_EQ(openingBid.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(openingBid.escrowedAmount, NOST_STANDARD_MIN_PRICE); - - const auto lowIncrement = nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT - 1, - NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT - 1); - EXPECT_EQ(lowIncrement.errorCode, NOST::EAuctionError::BidTooLow); - - const auto outbid = nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT, - NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT); - ASSERT_EQ(outbid.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(outbid.refundedAmount, NOST_STANDARD_MIN_PRICE); - - const auto bidderAState = nostromo.getParticipant(createOutput.auctionIndex, bidderA); - const auto bidderBState = nostromo.getParticipant(createOutput.auctionIndex, bidderB); - ASSERT_EQ(bidderAState.found, 1); - ASSERT_EQ(bidderBState.found, 1); - EXPECT_EQ(bidderAState.participantData.escrowedAmount, 0ULL); - EXPECT_EQ(bidderAState.participantData.isWinningBid, 0u); - EXPECT_EQ(bidderBState.participantData.escrowedAmount, NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT); - EXPECT_EQ(bidderBState.participantData.isWinningBid, 1u); - - const auto bidderBImprove = - nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 30000ULL, NOST_STANDARD_MIN_PRICE + 30000ULL); - EXPECT_EQ(bidderBImprove.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(bidderBImprove.refundedAmount, NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT); - EXPECT_EQ(bidderBImprove.escrowedAmount, NOST_STANDARD_MIN_PRICE + 30000ULL); - - nostromo.beginEpoch(); - EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); - const auto pausedBid = nostromo.placeBid(id(133, 134, 135, 136), createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 40000ULL, - NOST_STANDARD_MIN_PRICE + 40000ULL); - EXPECT_EQ(pausedBid.errorCode, NOST::EAuctionError::AuctionPaused); - nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); - - const auto resumedBid = nostromo.placeBid(id(137, 138, 139, 140), createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 40000ULL, - NOST_STANDARD_MIN_PRICE + 40000ULL); - EXPECT_EQ(resumedBid.errorCode, NOST::EAuctionError::Success); - - nostromo.setNow(2022, 4, 13, 12, 0, 0); - nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); - - const auto bootstrapPausedBid = nostromo.placeBid(id(141, 142, 143, 144), createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 50000ULL, - NOST_STANDARD_MIN_PRICE + 50000ULL); - EXPECT_EQ(bootstrapPausedBid.errorCode, NOST::EAuctionError::AuctionPaused); -} - -TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) -{ - { - ContractTestingNOST nostromo; - const id seller(141, 142, 143, 144); - const id allowed(145, 146, 147, 148); - const id denied(149, 150, 151, 152); - const uint64 assetName = assetNameFromString("PRIBID"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); - - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 3, 10); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowed}); - - const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - EXPECT_EQ(nostromo.placeBid(denied, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::PrivateAuctionAccessDenied); - EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(allowed, createOutput.auctionIndex, 1, 12).errorCode, NOST::EAuctionError::Success); - } - - { - ContractTestingNOST nostromo; - const id seller(153, 154, 155, 156); - const id gateIssuerA(157, 158, 159, 160); - const id gateIssuerB(161, 162, 163, 164); - const id belowThresholdBidder(165, 166, 167, 168); - const id exactThresholdBidder(169, 170, 171, 172); - const id alternateAssetBidder(173, 174, 175, 176); - const uint64 saleAssetName = assetNameFromString("PRIACS"); - const Asset saleAsset{seller, saleAssetName}; - const Asset accessAssetA{gateIssuerA, assetNameFromString("PRIAGA")}; - const Asset accessAssetB{gateIssuerB, assetNameFromString("PRIAGB")}; - - EXPECT_EQ(nostromo.issueAsset(seller, saleAssetName, 3), 3); - EXPECT_EQ(nostromo.issueAsset(gateIssuerA, accessAssetA.assetName, 5), 5); - EXPECT_EQ(nostromo.issueAsset(gateIssuerB, accessAssetB.assetName, 5), 5); - EXPECT_EQ(nostromo.transferAsset(gateIssuerA, belowThresholdBidder, accessAssetA, 2), 2); - EXPECT_EQ(nostromo.transferAsset(gateIssuerA, exactThresholdBidder, accessAssetA, 3), 3); - EXPECT_EQ(nostromo.transferAsset(gateIssuerB, alternateAssetBidder, accessAssetB, 5), 5); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, saleAsset, 3), 3); - - auto input = ContractTestingNOST::makeBatchAuctionInput(saleAsset, 3, 10); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.requiredAccessAssets = - ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAssetA, 3}, NOST::AuctionAssetEntry{accessAssetB, 5}}); - - const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - EXPECT_EQ(nostromo.placeBid(belowThresholdBidder, createOutput.auctionIndex, 1, 12, 12).errorCode, - NOST::EAuctionError::PrivateAuctionAccessDenied); - EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(exactThresholdBidder, createOutput.auctionIndex, 1, 12).errorCode, - NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(alternateAssetBidder, createOutput.auctionIndex, 1, 13).errorCode, - NOST::EAuctionError::Success); - } -} - -TEST(ContractNostromoAuction, PrivateAuctionCombinedAccessUsesInclusiveOrAuction) -{ - ContractTestingNOST nostromo; - const id seller(177, 178, 179, 180); - const id gateIssuer(181, 182, 183, 184); - const id walletOnlyBidder(185, 186, 187, 188); - const id assetOnlyBidder(189, 190, 191, 192); - const id bothBidder(193, 194, 195, 196); - const id deniedBidder(197, 198, 199, 200); - const uint64 saleAssetName = assetNameFromString("PRIORA"); - const Asset saleAsset{seller, saleAssetName}; - const Asset accessAsset{gateIssuer, assetNameFromString("PRIORG")}; - - EXPECT_EQ(nostromo.issueAsset(seller, saleAssetName, 3), 3); - EXPECT_EQ(nostromo.issueAsset(gateIssuer, accessAsset.assetName, 2), 2); - EXPECT_EQ(nostromo.transferAsset(gateIssuer, assetOnlyBidder, accessAsset, 1), 1); - EXPECT_EQ(nostromo.transferAsset(gateIssuer, bothBidder, accessAsset, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, saleAsset, 3), 3); - - auto input = ContractTestingNOST::makeBatchAuctionInput(saleAsset, 3, 10); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({walletOnlyBidder, bothBidder}); - input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAsset, 1}}); - - const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - const auto auctionOutput = nostromo.getAuction(createOutput.auctionIndex); - EXPECT_EQ(auctionOutput.auction.allowedBidderWalletCount, 2U); - EXPECT_EQ(auctionOutput.auction.requiredAccessAssetCount, 1U); - - nostromo.seedUser(deniedBidder, 100); - const sint64 deniedBalanceBefore = getBalance(deniedBidder); - const sint64 contractBalanceBeforeDeniedBid = getBalance(NOST_CONTRACT_ID); - EXPECT_EQ(nostromo.placeBidWithFundedReward(deniedBidder, createOutput.auctionIndex, 1, 12, 12).errorCode, - NOST::EAuctionError::PrivateAuctionAccessDenied); - EXPECT_EQ(getBalance(deniedBidder), deniedBalanceBefore); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBalanceBeforeDeniedBid); - EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, deniedBidder).found, 0); - EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(walletOnlyBidder, createOutput.auctionIndex, 1, 12).errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(assetOnlyBidder, createOutput.auctionIndex, 1, 13).errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(bothBidder, createOutput.auctionIndex, 1, 14).errorCode, NOST::EAuctionError::Success); -} - -TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) -{ - ContractTestingNOST nostromo; - const id seller(171, 172, 173, 174); - const id bidder(175, 176, 177, 178); - const uint64 assetName = assetNameFromString("BUYNWA"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); - - auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 3), NOST_STANDARD_MIN_PRICE, - NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT, - NOST_STANDARD_MIN_PRICE + 800000ULL); - const auto createOutput = nostromo.createAuction(seller, input); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - const sint64 sellerBalanceBefore = getBalance(seller); - - const auto bidOutput = - nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 800000ULL, NOST_STANDARD_MIN_PRICE + 800000ULL); - ASSERT_EQ(bidOutput.errorCode, NOST::EAuctionError::Success); - - const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; - const auto participant = nostromo.getParticipant(createOutput.auctionIndex, bidder); - EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(auction.core.allocatedQuantity, 1ULL); - ASSERT_EQ(participant.found, 1); - EXPECT_EQ(participant.participantData.allocatedQuantity, 1ULL); - EXPECT_EQ(participant.participantData.escrowedAmount, 0ULL); - EXPECT_EQ(participant.participantData.isWinningBid, 1u); - EXPECT_EQ(nostromo.managedShares(asset, bidder), 3); - EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, 1683000ULL); -} - -TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialFillAuction) -{ - { - ContractTestingNOST nostromo; - const id seller(181, 182, 183, 184); - const id bidderA(185, 186, 187, 188); - const id bidderB(189, 190, 191, 192); - const id bidderC(193, 194, 195, 196); - const uint64 assetName = assetNameFromString("BATFIN"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 4), 4); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 4), 4); - - const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 4, 10)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 3, 15).errorCode, NOST::EAuctionError::Success); - nostromo.setNow(2026, 1, 1, 9, 0, 1); - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 1, 15).errorCode, NOST::EAuctionError::Success); - nostromo.setNow(2026, 1, 1, 9, 0, 2); - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderC, createOutput.auctionIndex, 2, 20).errorCode, NOST::EAuctionError::Success); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - - const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; - const auto participantA = nostromo.getParticipant(createOutput.auctionIndex, bidderA); - const auto participantB = nostromo.getParticipant(createOutput.auctionIndex, bidderB); - const auto participantC = nostromo.getParticipant(createOutput.auctionIndex, bidderC); - - EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(auction.core.allocatedQuantity, 4ULL); - ASSERT_EQ(participantA.found, 1); - ASSERT_EQ(participantB.found, 1); - ASSERT_EQ(participantC.found, 1); - EXPECT_EQ(participantC.participantData.allocatedQuantity, 2ULL); - EXPECT_EQ(participantA.participantData.allocatedQuantity, 2ULL); - EXPECT_EQ(participantB.participantData.allocatedQuantity, 0ULL); - EXPECT_EQ(participantA.participantData.escrowedAmount, 0ULL); - EXPECT_EQ(participantB.participantData.escrowedAmount, 0ULL); - EXPECT_EQ(participantC.participantData.escrowedAmount, 0ULL); - EXPECT_EQ(participantA.participantData.isWinningBid, 1u); - EXPECT_EQ(participantB.participantData.isWinningBid, 0u); - EXPECT_EQ(participantC.participantData.isWinningBid, 1u); - EXPECT_EQ(nostromo.managedShares(asset, bidderA), 2); - EXPECT_EQ(nostromo.managedShares(asset, bidderB), 0); - EXPECT_EQ(nostromo.managedShares(asset, bidderC), 2); - EXPECT_EQ(nostromo.managedShares(asset, seller), 0); - } - - { - ContractTestingNOST nostromo; - const id seller(197, 198, 199, 200); - const id bidder(201, 202, 203, 204); - const uint64 assetName = assetNameFromString("BATRET"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 5), 5); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 5), 5); - - const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 5, 10)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, 2, 12).errorCode, NOST::EAuctionError::Success); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - - const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; - EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(auction.core.allocatedQuantity, 2ULL); - EXPECT_EQ(nostromo.managedShares(asset, bidder), 2); - EXPECT_EQ(nostromo.managedShares(asset, seller), 3); - } - - { - ContractTestingNOST nostromo; - const id seller(313, 314, 315, 316); - const id firstBidder(317, 318, 319, 320); - const id secondBidder(321, 322, 323, 324); - const Asset asset{seller, assetNameFromString("BATPRTL")}; - - EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 15), 15); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 15), 15); - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 15, 10); - input.minimumPurchaseQuantity = 10; - const auto createOutput = nostromo.createAuction(seller, input); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - const sint64 sellerBalanceBefore = getBalance(seller); - nostromo.seedUser(firstBidder, 291); - nostromo.seedUser(secondBidder, 150); - const sint64 secondBidderBalanceBefore = getBalance(secondBidder); - - ASSERT_EQ(nostromo.placeBatchBidWithFundedRequiredReward(firstBidder, createOutput.auctionIndex, 10, 20).errorCode, - NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBidWithFundedReward(secondBidder, createOutput.auctionIndex, 10, 15, 150).errorCode, NOST::EAuctionError::BidTooLow); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - - const auto firstParticipant = nostromo.getParticipant(createOutput.auctionIndex, firstBidder); - const auto secondParticipant = nostromo.getParticipant(createOutput.auctionIndex, secondBidder); - ASSERT_EQ(firstParticipant.found, 1); - ASSERT_EQ(secondParticipant.found, 0); - EXPECT_EQ(firstParticipant.participantData.allocatedQuantity, 10ULL); - EXPECT_EQ(nostromo.managedShares(asset, firstBidder), 10); - EXPECT_EQ(nostromo.managedShares(asset, secondBidder), 0); - EXPECT_EQ(nostromo.managedShares(asset, seller), 5); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.allocatedQuantity, 10ULL); - EXPECT_EQ(getBalance(secondBidder), secondBidderBalanceBefore); - EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, 187ULL); - } - - { - ContractTestingNOST nostromo; - const id seller(325, 326, 327, 328); - const id firstBidder(329, 330, 331, 332); - const id partialBidder(333, 334, 335, 336); - const Asset asset{seller, assetNameFromString("BATPMIN")}; - - EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 22), 22); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 22), 22); - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 22, 10); - input.minimumPurchaseQuantity = 10; - const auto createOutput = nostromo.createAuction(seller, input); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(firstBidder, createOutput.auctionIndex, 10, 22).errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(partialBidder, createOutput.auctionIndex, 15, 22, 225).errorCode, NOST::EAuctionError::QuantityUnavailable); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - - const auto partialParticipant = nostromo.getParticipant(createOutput.auctionIndex, partialBidder); - ASSERT_EQ(partialParticipant.found, 0); - EXPECT_EQ(nostromo.managedShares(asset, partialBidder), 0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.allocatedQuantity, 10ULL); - } -} - -TEST(ContractNostromoAuction, BatchFinalizationRefundsLosingBidsAndTieBreaksByBidTimeAuction) -{ - ContractTestingNOST nostromo; - const id seller(205, 206, 207, 208); - const id earlierBidder(209, 210, 211, 212); - const id laterBidder(213, 214, 215, 216); - const id higherBidder(217, 218, 219, 220); - const uint64 assetName = assetNameFromString("BATTIE"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); - - const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 5)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - nostromo.seedUser(earlierBidder, 100); - nostromo.seedUser(laterBidder, 100); - nostromo.seedUser(higherBidder, 100); - const sint64 earlierBefore = getBalance(earlierBidder); - const sint64 laterBefore = getBalance(laterBidder); - const sint64 higherBefore = getBalance(higherBidder); - - ASSERT_EQ(nostromo.placeBatchBidWithFundedRequiredReward(earlierBidder, createOutput.auctionIndex, 1, 10).errorCode, - NOST::EAuctionError::Success); - nostromo.setNow(2026, 1, 1, 9, 0, 1); - ASSERT_EQ(nostromo.placeBatchBidWithFundedRequiredReward(laterBidder, createOutput.auctionIndex, 1, 10).errorCode, NOST::EAuctionError::Success); - nostromo.setNow(2026, 1, 1, 9, 0, 2); - ASSERT_EQ(nostromo.placeBatchBidWithFundedRequiredReward(higherBidder, createOutput.auctionIndex, 1, 11).errorCode, NOST::EAuctionError::Success); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - - const auto earlier = nostromo.getParticipant(createOutput.auctionIndex, earlierBidder); - const auto later = nostromo.getParticipant(createOutput.auctionIndex, laterBidder); - const auto higher = nostromo.getParticipant(createOutput.auctionIndex, higherBidder); - ASSERT_EQ(earlier.found, 1); - ASSERT_EQ(later.found, 1); - ASSERT_EQ(higher.found, 1); - EXPECT_EQ(earlier.participantData.allocatedQuantity, 1ULL); - EXPECT_EQ(later.participantData.allocatedQuantity, 0ULL); - EXPECT_EQ(higher.participantData.allocatedQuantity, 1ULL); - EXPECT_EQ(earlier.participantData.escrowedAmount, 0ULL); - EXPECT_EQ(later.participantData.escrowedAmount, 0ULL); - EXPECT_EQ(higher.participantData.escrowedAmount, 0ULL); - EXPECT_EQ(nostromo.managedShares(asset, earlierBidder), 1); - EXPECT_EQ(nostromo.managedShares(asset, laterBidder), 0); - EXPECT_EQ(nostromo.managedShares(asset, higherBidder), 1); - EXPECT_EQ(getBalance(earlierBidder), earlierBefore - 100); - EXPECT_EQ(getBalance(laterBidder), laterBefore - 90); - EXPECT_EQ(getBalance(higherBidder), higherBefore - 100); -} - -TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) -{ - ContractTestingNOST nostromo; - const id seller(211, 212, 213, 214); - const uint64 assetName = assetNameFromString("STDNOB"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - - const auto createOutput = - nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - - const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; - EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(auction.core.allocatedQuantity, 0ULL); - EXPECT_TRUE(isZero(auction.core.highestBidder)); - EXPECT_EQ(nostromo.managedShares(asset, seller), 1); -} - -TEST(ContractNostromoAuction, WeeklyPauseShiftsActiveAuctionDeadlineAuction) -{ - ContractTestingNOST nostromo; - const id seller(215, 216, 217, 218); - const uint64 assetName = assetNameFromString("PAUSHL"); - const Asset asset{seller, assetName}; - - nostromo.setNow(2026, 1, 6, 11, 40, 0); - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - - const auto createOutput = - nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - nostromo.setNow(2026, 1, 7, 11, 40, 0); - nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); - - nostromo.setNow(2026, 1, 7, 12, 0, 0); - nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); - - nostromo.setNow(2026, 1, 7, 12, 10, 1); - nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(nostromo.managedShares(asset, seller), 1); -} +#include "contract_testing.h" -TEST(ContractNostromoAuction, EndTickSkipsAuctionProcessingAtBootstrapTimeAuction) -{ - ContractTestingNOST nostromo; - const id seller(215, 216, 217, 218); - const uint64 assetName = assetNameFromString("BOOTTK"); - const Asset asset{seller, assetName}; - - nostromo.setNow(2022, 4, 12, 12, 0, 0); - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - - const auto createOutput = - nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - nostromo.setNow(2022, 4, 13, 12, 0, 0); - nostromo.advanceAndEndTick(0); - - const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; - EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Active); - EXPECT_EQ(auction.core.allocatedQuantity, 0ULL); - EXPECT_EQ(nostromo.managedShares(asset, seller), 0); -} +static std::mt19937_64 rand64; -TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) +static unsigned long long random(unsigned long long minValue, unsigned long long maxValue) { - ContractTestingNOST nostromo; - const id seller(219, 220, 221, 222); - const id bidder(223, 224, 225, 226); - const uint64 assetName = assetNameFromString("PDSHFT"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - - const auto createOutput = nostromo.createAuction( - seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, - NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ( - nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 200000ULL, NOST_STANDARD_MIN_PRICE + 200000ULL).errorCode, - NOST::EAuctionError::Success); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY) * 1000ULL); - auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; - const auto originalSellerDecisionDeadline = auction.core.sellerDecisionDeadline; - ASSERT_EQ(auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); - EXPECT_EQ(auction.core.sellerDecisionDeadline.getHour(), 9); - EXPECT_EQ(auction.core.sellerDecisionDeadline.getMinute(), 0); - EXPECT_EQ(auction.core.sellerDecisionDeadline.getSecond(), 0); - - nostromo.setNow(2026, 1, 9, 8, 59, 50); - nostromo.beginEpoch(); - const uint32 launchPauseTicksAfterBeginEpoch = nostromo.getTicksBeforeAuctionLaunch().ticks; - EXPECT_EQ(launchPauseTicksAfterBeginEpoch, NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); - - nostromo.advanceAndEndTick(1000); - EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, launchPauseTicksAfterBeginEpoch - 1); - - nostromo.setNow(2026, 1, 9, 9, 8, 10); - nostromo.advanceAndEndTick(0); - auction = nostromo.getAuction(createOutput.auctionIndex).auction; - ASSERT_EQ(auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); - EXPECT_EQ(auction.core.sellerDecisionDeadline, originalSellerDecisionDeadline); - - nostromo.advanceTicks(launchPauseTicksAfterBeginEpoch - 2); - auction = nostromo.getAuction(createOutput.auctionIndex).auction; - ASSERT_EQ(auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); - EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, 0U); - EXPECT_GT(auction.core.sellerDecisionDeadline, originalSellerDecisionDeadline); - - auto shiftedDeadline = auction.core.sellerDecisionDeadline; - shiftedDeadline.add(0, 0, 0, 0, 0, -1); - nostromo.setNow(shiftedDeadline.getYear(), shiftedDeadline.getMonth(), shiftedDeadline.getDay(), shiftedDeadline.getHour(), - shiftedDeadline.getMinute(), shiftedDeadline.getSecond()); - nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); - - shiftedDeadline = auction.core.sellerDecisionDeadline; - shiftedDeadline.add(0, 0, 0, 0, 0, 1); - nostromo.setNow(shiftedDeadline.getYear(), shiftedDeadline.getMonth(), shiftedDeadline.getDay(), shiftedDeadline.getHour(), - shiftedDeadline.getMinute(), shiftedDeadline.getSecond()); - nostromo.advanceAndEndTick(0); - auction = nostromo.getAuction(createOutput.auctionIndex).auction; - EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); + if(minValue > maxValue) + { + return 0; + } + return minValue + rand64() % (maxValue - minValue); } -TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) +static id getUser(unsigned long long i) { - const uint8 routeModes[] = {0, 1}; - for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) - { - SCOPED_TRACE(::testing::Message() << "routeAllFeesToDevelopment=" << static_cast(routeModes[routeIndex])); - ContractTestingNOST nostromo; - const uint8 routeMode = routeModes[routeIndex]; - const id seller(221 + routeIndex, 222 + routeIndex, 223 + routeIndex, 224 + routeIndex); - const id bidder(225 + routeIndex, 226 + routeIndex, 227 + routeIndex, 228 + routeIndex); - const uint64 assetName = assetNameFromString(routeMode ? "STDENR1" : "STDENR0"); - const Asset asset{seller, assetName}; - - nostromo.setRouteAllFeesToDevelopment(routeMode); - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - - const auto createOutput = - nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - // Isolate the sale-fee pool from the auction creation fee. - nostromo.endEpoch(); - constexpr uint64 expectedSellerPayout = 935000ULL; - constexpr uint64 expectedShareholderDividend = 45000ULL; - constexpr uint64 expectedManagementFee = 5000ULL; - constexpr uint64 expectedDevelopmentFee = 5000ULL; - constexpr uint64 expectedCoordinatorFee = 10000ULL; - constexpr uint64 expectedTotalFees = 65000ULL; - const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedShareholderDividend); - const sint64 sellerBalanceBefore = getBalance(seller); - const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); - const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); - const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); - const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, - NOST::EAuctionError::Success); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - - const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; - EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(auction.core.allocatedQuantity, 1ULL); - EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); - EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, expectedSellerPayout); - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedTotalFees); - EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, expectedTotalFees); - - nostromo.endEpoch(); - EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 0ULL); - - if (routeMode != 0) - { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedTotalFees); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); - } - else - { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedManagementFee); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedDevelopmentFee); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedCoordinatorFee); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); - } - } + return id(i, i / 2 + 4, i + 10, i * 3 + 8); } -TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction) +static std::vector getRandomUsers(unsigned int totalUsers, unsigned int maxNum) { - { - ContractTestingNOST nostromo; - const id seller(231, 232, 233, 234); - const id bidder(235, 236, 237, 238); - const uint64 assetName = assetNameFromString("PENACC"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - - const auto createOutput = nostromo.createAuction( - seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, - NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 200000ULL, NOST_STANDARD_MIN_PRICE + 200000ULL) - .errorCode, - NOST::EAuctionError::Success); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); - - const auto forbidden = nostromo.resolvePendingStandardAuction(id(999, 999, 999, 999), createOutput.auctionIndex, true); - EXPECT_EQ(forbidden.errorCode, NOST::EAuctionError::Forbidden); - - const auto acceptOutput = nostromo.resolvePendingStandardAuction(seller, createOutput.auctionIndex, true); - EXPECT_EQ(acceptOutput.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); - } - - { - ContractTestingNOST nostromo; - const id seller(239, 240, 241, 242); - const id bidder(243, 244, 245, 246); - const uint64 assetName = assetNameFromString("PENREJ"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - - const auto createOutput = nostromo.createAuction( - seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, - NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - nostromo.seedUser(bidder, NOST_STANDARD_MIN_PRICE + 300000ULL); - const sint64 bidderBeforeBid = getBalance(bidder); - ASSERT_EQ(nostromo - .placeBidWithFundedReward(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 200000ULL, - NOST_STANDARD_MIN_PRICE + 200000ULL) - .errorCode, - NOST::EAuctionError::Success); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto rejectOutput = nostromo.resolvePendingStandardAuction(seller, createOutput.auctionIndex, false); - EXPECT_EQ(rejectOutput.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(rejectOutput.refundedAmount, NOST_STANDARD_MIN_PRICE + 200000ULL); - - const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; - const auto participant = nostromo.getParticipant(createOutput.auctionIndex, bidder); - EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(auction.core.allocatedQuantity, 0ULL); - EXPECT_TRUE(isZero(auction.core.highestBidder)); - ASSERT_EQ(participant.found, 1); - EXPECT_EQ(participant.participantData.allocatedQuantity, 0ULL); - EXPECT_EQ(participant.participantData.escrowedAmount, 0ULL); - EXPECT_EQ(nostromo.managedShares(asset, seller), 1); - EXPECT_EQ(getBalance(bidder), bidderBeforeBid); - } - - { - ContractTestingNOST nostromo; - const id seller(247, 248, 249, 250); - const id bidder(251, 252, 253, 254); - const uint64 assetName = assetNameFromString("PENTMO"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - - const auto createOutput = nostromo.createAuction( - seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, - NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 200000ULL, NOST_STANDARD_MIN_PRICE + 200000ULL) - .errorCode, - NOST::EAuctionError::Success); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); - - nostromo.advanceAndEndTick((NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; - const auto participant = nostromo.getParticipant(createOutput.auctionIndex, bidder); - EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(auction.core.allocatedQuantity, 1ULL); - ASSERT_EQ(participant.found, 1); - EXPECT_EQ(participant.participantData.allocatedQuantity, 1ULL); - EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); - } + unsigned long long userCount = random(0, maxNum); + std::vector users; + users.reserve(userCount); + for (unsigned int i = 0; i < userCount; ++i) + { + unsigned long long userIdx = random(0, totalUsers - 1); + users.push_back(getUser(userIdx)); + } + return users; } -TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) +class NostromoChecker : public NOST, public NOST::StateData { - const uint8 routeModes[] = {0, 1}; - for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) - { - SCOPED_TRACE(::testing::Message() << "routeAllFeesToDevelopment=" << static_cast(routeModes[routeIndex])); - { - ContractTestingNOST nostromo; - const uint8 routeMode = routeModes[routeIndex]; - const id seller(261 + routeIndex, 262 + routeIndex, 263 + routeIndex, 264 + routeIndex); - const uint64 assetName = assetNameFromString(routeMode ? "CANBT1" : "CANBT0"); - const Asset asset{seller, assetName}; - - nostromo.setRouteAllFeesToDevelopment(routeMode); - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 10), 10); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); - - const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 1000)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - nostromo.endEpoch(); - const sint64 sellerBefore = getBalance(seller); - const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); - const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); - const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); - const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - constexpr uint64 expectedShareholderDividend = 727ULL; - constexpr uint64 expectedRecipientFee = 91ULL; - const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedShareholderDividend); - - const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionIndex, 1000); - EXPECT_EQ(cancelOutput.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(cancelOutput.refundedAmount, 0ULL); - EXPECT_EQ(cancelOutput.cancellationFee, 1000ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Cancelled); - EXPECT_EQ(nostromo.managedShares(asset, seller), 10); - EXPECT_EQ(getBalance(seller) - sellerBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 1000ULL); - EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 1000ULL); - nostromo.endEpoch(); - if (routeMode != 0) - { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 1000ULL); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); - } - else - { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedRecipientFee); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedRecipientFee); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedRecipientFee); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); - } - } - - { - ContractTestingNOST nostromo; - const uint8 routeMode = routeModes[routeIndex]; - const id seller(273 + routeIndex, 274 + routeIndex, 275 + routeIndex, 276 + routeIndex); - const uint64 assetName = assetNameFromString(routeMode ? "CANST1" : "CANST0"); - const Asset asset{seller, assetName}; - - nostromo.setRouteAllFeesToDevelopment(routeMode); - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - - const auto createOutput = - nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - nostromo.endEpoch(); - const sint64 sellerBefore = getBalance(seller); - const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); - const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); - const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); - const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - constexpr uint64 expectedShareholderDividend = 72700ULL; - constexpr uint64 expectedRecipientFee = 9100ULL; - const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedShareholderDividend); - - const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionIndex, 100000); - EXPECT_EQ(cancelOutput.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(cancelOutput.refundedAmount, 0ULL); - EXPECT_EQ(cancelOutput.cancellationFee, 100000ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Cancelled); - EXPECT_EQ(nostromo.managedShares(asset, seller), 1); - EXPECT_EQ(getBalance(seller) - sellerBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 100000ULL); - EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 100000ULL); - nostromo.endEpoch(); - if (routeMode != 0) - { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 100000ULL); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); - } - else - { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedRecipientFee); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedRecipientFee); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedRecipientFee); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); - } - } - } -} - -TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeRemainderAuction) -{ - const uint8 routeModes[] = {0, 1}; - for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) - { - SCOPED_TRACE(::testing::Message() << "routeAllFeesToDevelopment=" << static_cast(routeModes[routeIndex])); - { - ContractTestingNOST nostromo; - const uint8 routeMode = routeModes[routeIndex]; - const id batchSeller(277 + routeIndex, 278 + routeIndex, 279 + routeIndex, 280 + routeIndex); - const uint64 batchAssetName = assetNameFromString(routeMode ? "CANRN1" : "CANRN0"); - const Asset batchAsset{batchSeller, batchAssetName}; - - nostromo.setRouteAllFeesToDevelopment(routeMode); - EXPECT_EQ(nostromo.issueAsset(batchSeller, batchAssetName, 7), 7); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(batchSeller, batchAsset, 7), 7); - - const auto batchCreateOutput = nostromo.createAuction(batchSeller, ContractTestingNOST::makeBatchAuctionInput(batchAsset, 7, 333)); - ASSERT_EQ(batchCreateOutput.errorCode, NOST::EAuctionError::Success); - nostromo.endEpoch(); - - constexpr uint64 expectedBatchShareholderDividend = 170ULL; - constexpr uint64 expectedBatchRecipientFee = 21ULL; - const sint64 expectedBatchDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedBatchShareholderDividend); - const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); - const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); - const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); - const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - - const auto batchCancelOutput = nostromo.cancelAuction(batchSeller, batchCreateOutput.auctionIndex, 233); - EXPECT_EQ(batchCancelOutput.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(batchCancelOutput.cancellationFee, 233ULL); - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 233ULL); - EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 233ULL); - nostromo.endEpoch(); - if (routeMode != 0) - { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 233ULL); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); - } - else - { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedBatchRecipientFee); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedBatchRecipientFee); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedBatchRecipientFee); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedBatchDividendPoolIncrease); - } - EXPECT_EQ(expectedBatchShareholderDividend + expectedBatchRecipientFee * 3ULL, batchCancelOutput.cancellationFee); - } - +public: + void registerChecker(id registerId, uint32 tierLevel, uint32 indexOfRegister) + { + EXPECT_EQ(users.contains(registerId), 1); + uint8 stateTierLevel; + users.get(registerId, stateTierLevel); + EXPECT_EQ(tierLevel, stateTierLevel); + } + void countOfRegisterChecker(uint32 totalUser) + { + EXPECT_EQ(totalUser, numberOfRegister); + } + void logoutFromTierChecker(id registerId) + { + EXPECT_EQ(users.contains(registerId), 0); + } + void numberOfCreatedProjectChecker(uint32 numberOfProjects) + { + EXPECT_EQ(numberOfProjects, numberOfCreatedProject); + } + void createdProjectChecker(uint32 indexOfProject, id creator, uint64 assetName, uint32 supply, uint32 startYear, uint32 startMonth, uint32 startDay, uint32 startHour, uint32 endYear, uint32 endMonth, uint32 endDay, uint32 endHour) + { + uint32 startDate, endDate; + NOST::packNostromoDate(startYear, startMonth, startDay, startHour, 0, 0, startDate); + NOST::packNostromoDate(endYear, endMonth, endDay, endHour, 0, 0, endDate); + + EXPECT_EQ(tokens.contains(assetName), 1); + EXPECT_EQ(projects.get(indexOfProject).creator, creator); + EXPECT_EQ(projects.get(indexOfProject).isCreatedFundarasing, 0); + EXPECT_EQ(projects.get(indexOfProject).numberOfNo, 0); + EXPECT_EQ(projects.get(indexOfProject).numberOfYes, 0); + EXPECT_EQ(projects.get(indexOfProject).supplyOfToken, supply); + EXPECT_EQ(projects.get(indexOfProject).tokenName, assetName); + EXPECT_EQ(projects.get(indexOfProject).startDate, startDate); + EXPECT_EQ(projects.get(indexOfProject).endDate, endDate); + } + void epochRevenueChecker(uint64 amountOfRevenue) + { + EXPECT_EQ(amountOfRevenue, epochRevenue); + } + void totalPoolWeightChecker(uint32 totalWeight) + { + EXPECT_EQ(totalWeight, totalPoolWeight); + } + void voteInProjectChecker(uint32 indexOfProject, uint32 numberOfYes, uint32 numberOfNo) + { + EXPECT_EQ(projects.get(indexOfProject).numberOfYes, numberOfYes); + EXPECT_EQ(projects.get(indexOfProject).numberOfNo, numberOfNo); + } + void numberOfVotedProjectAndVotedListChecker(id registerId, uint32 numberOfProject, Array votedList) + { + uint32 count; + numberOfVotedProject.get(registerId, count); + EXPECT_EQ(count, numberOfProject); + + Array vote; + voteStatus.get(registerId, vote); + for (uint32 i = 0; i < count; i++) + { + EXPECT_EQ(vote.get(i), votedList.get(i)); + } + } + void countOfFundraisingChecker(uint32 count) + { + EXPECT_EQ(count, numberOfFundraising); + } + void createFundraisingChecker(const id& registerId, + uint64 tokenPrice, + uint64 soldAmount, + uint64 requiredFunds, + + uint32 indexOfProject, + uint32 firstPhaseStartYear, + uint32 firstPhaseStartMonth, + uint32 firstPhaseStartDay, + uint32 firstPhaseStartHour, + uint32 firstPhaseEndYear, + uint32 firstPhaseEndMonth, + uint32 firstPhaseEndDay, + uint32 firstPhaseEndHour, + + uint32 secondPhaseStartYear, + uint32 secondPhaseStartMonth, + uint32 secondPhaseStartDay, + uint32 secondPhaseStartHour, + uint32 secondPhaseEndYear, + uint32 secondPhaseEndMonth, + uint32 secondPhaseEndDay, + uint32 secondPhaseEndHour, + + uint32 thirdPhaseStartYear, + uint32 thirdPhaseStartMonth, + uint32 thirdPhaseStartDay, + uint32 thirdPhaseStartHour, + uint32 thirdPhaseEndYear, + uint32 thirdPhaseEndMonth, + uint32 thirdPhaseEndDay, + uint32 thirdPhaseEndHour, + + uint32 listingStartYear, + uint32 listingStartMonth, + uint32 listingStartDay, + uint32 listingStartHour, + + uint32 cliffEndYear, + uint32 cliffEndMonth, + uint32 cliffEndDay, + uint32 cliffEndHour, + + uint32 vestingEndYear, + uint32 vestingEndMonth, + uint32 vestingEndDay, + uint32 vestingEndHour, + + uint8 threshold, + uint8 TGE, + uint8 stepOfVesting, + + uint32 indexOfFundraising) + { + uint32 firstPhaseStartDate_t, secondPhaseStartDate_t, thirdPhaseStartDate_t, firstPhaseEndDate_t, secondPhaseEndDate_t, thirdPhaseEndDate_t, listingStartDate_t, cliffEndDate_t, vestingEndDate_t; + NOST::packNostromoDate(firstPhaseStartYear, firstPhaseStartMonth, firstPhaseStartDay, firstPhaseStartHour, 0, 0, firstPhaseStartDate_t); + NOST::packNostromoDate(secondPhaseStartYear, secondPhaseStartMonth, secondPhaseStartDay, secondPhaseStartHour, 0, 0, secondPhaseStartDate_t); + NOST::packNostromoDate(thirdPhaseStartYear, thirdPhaseStartMonth, thirdPhaseStartDay, thirdPhaseStartHour, 0, 0, thirdPhaseStartDate_t); + NOST::packNostromoDate(firstPhaseEndYear, firstPhaseEndMonth, firstPhaseEndDay, firstPhaseEndHour, 0, 0, firstPhaseEndDate_t); + NOST::packNostromoDate(secondPhaseEndYear, secondPhaseEndMonth, secondPhaseEndDay, secondPhaseEndHour, 0, 0, secondPhaseEndDate_t); + NOST::packNostromoDate(thirdPhaseEndYear, thirdPhaseEndMonth, thirdPhaseEndDay, thirdPhaseEndHour, 0, 0, thirdPhaseEndDate_t); + NOST::packNostromoDate(listingStartYear, listingStartMonth, listingStartDay, listingStartHour, 0, 0, listingStartDate_t); + NOST::packNostromoDate(cliffEndYear, cliffEndMonth, cliffEndDay, cliffEndHour, 0, 0, cliffEndDate_t); + NOST::packNostromoDate(vestingEndYear, vestingEndMonth, vestingEndDay, vestingEndHour, 0, 0, vestingEndDate_t); + + EXPECT_EQ(registerId, projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator); + EXPECT_EQ(tokenPrice, fundaraisings.get(indexOfFundraising).tokenPrice); + + EXPECT_EQ(soldAmount, fundaraisings.get(indexOfFundraising).soldAmount); + EXPECT_EQ(requiredFunds, fundaraisings.get(indexOfFundraising).requiredFunds); + EXPECT_EQ(indexOfProject, fundaraisings.get(indexOfFundraising).indexOfProject); + EXPECT_EQ(firstPhaseStartDate_t, fundaraisings.get(indexOfFundraising).firstPhaseStartDate); + EXPECT_EQ(secondPhaseStartDate_t, fundaraisings.get(indexOfFundraising).secondPhaseStartDate); + EXPECT_EQ(thirdPhaseStartDate_t, fundaraisings.get(indexOfFundraising).thirdPhaseStartDate); + EXPECT_EQ(firstPhaseEndDate_t, fundaraisings.get(indexOfFundraising).firstPhaseEndDate); + EXPECT_EQ(secondPhaseEndDate_t, fundaraisings.get(indexOfFundraising).secondPhaseEndDate); + EXPECT_EQ(thirdPhaseEndDate_t, fundaraisings.get(indexOfFundraising).thirdPhaseEndDate); + EXPECT_EQ(listingStartDate_t, fundaraisings.get(indexOfFundraising).listingStartDate); + EXPECT_EQ(cliffEndDate_t, fundaraisings.get(indexOfFundraising).cliffEndDate); + EXPECT_EQ(vestingEndDate_t, fundaraisings.get(indexOfFundraising).vestingEndDate); + EXPECT_EQ(threshold, fundaraisings.get(indexOfFundraising).threshold); + EXPECT_EQ(TGE, fundaraisings.get(indexOfFundraising).TGE); + EXPECT_EQ(stepOfVesting, fundaraisings.get(indexOfFundraising).stepOfVesting); + + } + uint8 getTierLevel(id registerId) + { + if (users.contains(registerId)) + { + uint8 tierLevel; + users.get(registerId, tierLevel); + return tierLevel; + } + return 0; + } + uint64 getInvestedAmount(uint32 indexOfFundraising, id registerId) + { + investors.get(registerId, tmpInvestedList); + uint32 numberOfProject; + numberOfInvestedProjects.get(registerId, numberOfProject); + + for (uint32 i = 0; i < numberOfProject; i++) { - ContractTestingNOST smallFeeNostromo; - const uint8 routeMode = routeModes[routeIndex]; - const id standardSeller(281 + routeIndex, 282 + routeIndex, 283 + routeIndex, 284 + routeIndex); - const uint64 standardAssetName = assetNameFromString(routeMode ? "CANON1" : "CANON0"); - const Asset standardAsset{standardSeller, standardAssetName}; - - smallFeeNostromo.setRouteAllFeesToDevelopment(routeMode); - EXPECT_EQ(smallFeeNostromo.issueAsset(standardSeller, standardAssetName, 1), 1); - EXPECT_EQ(smallFeeNostromo.transferShareManagementRightsToNostromo(standardSeller, standardAsset, 1), 1); - - const auto standardCreateOutput = - smallFeeNostromo.createAuction(standardSeller, ContractTestingNOST::makeBatchAuctionInput(standardAsset, 1, 19)); - ASSERT_EQ(standardCreateOutput.errorCode, NOST::EAuctionError::Success); - smallFeeNostromo.endEpoch(); - - const sint64 expectedSmallDividendPoolIncrease = smallFeeNostromo.expectedDividendPoolIncrease(1ULL); - const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); - const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - - const auto standardCancelOutput = smallFeeNostromo.cancelAuction(standardSeller, standardCreateOutput.auctionIndex, 1); - EXPECT_EQ(standardCancelOutput.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(standardCancelOutput.cancellationFee, 1ULL); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 1ULL); - EXPECT_EQ(smallFeeNostromo.getNostromoFeePool().totalAmount, 1ULL); - smallFeeNostromo.endEpoch(); - if (routeMode != 0) - { - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 1ULL); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); - } - else + if (tmpInvestedList.get(i).indexOfFundraising == indexOfFundraising) { - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedSmallDividendPoolIncrease); + return tmpInvestedList.get(i).investedAmount; } } - } -} - -TEST(ContractNostromoAuction, CancelAuctionRejectsAfterAcceptedBidWithoutSideEffectsAuction) -{ - { - ContractTestingNOST nostromo; - const id seller(281, 282, 283, 284); - const id bidder(285, 286, 287, 288); - const uint64 assetName = assetNameFromString("CANINV"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); - - const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, 1, 12).errorCode, NOST::EAuctionError::Success); - - const auto notFound = nostromo.cancelAuction(seller, 800, 10); - EXPECT_EQ(notFound.errorCode, NOST::EAuctionError::AuctionNotFound); - - const auto forbidden = nostromo.cancelAuction(bidder, createOutput.auctionIndex, 10); - EXPECT_EQ(forbidden.errorCode, NOST::EAuctionError::Forbidden); - - const auto bidderBalanceBeforeCancel = getBalance(bidder); - const auto participantBeforeCancel = nostromo.getParticipant(createOutput.auctionIndex, bidder); - const auto rejected = nostromo.cancelAuction(seller, createOutput.auctionIndex, 0); - EXPECT_EQ(rejected.errorCode, NOST::EAuctionError::AuctionHasAcceptedBid); - EXPECT_EQ(rejected.refundedAmount, 0ULL); - EXPECT_EQ(getBalance(bidder), bidderBalanceBeforeCancel); - const auto participantAfterCancel = nostromo.getParticipant(createOutput.auctionIndex, bidder); - ASSERT_EQ(participantBeforeCancel.found, 1); - ASSERT_EQ(participantAfterCancel.found, 1); - EXPECT_EQ(participantAfterCancel.participantData.escrowedAmount, participantBeforeCancel.participantData.escrowedAmount); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); - EXPECT_EQ(nostromo.managedShares(asset, NOST_CONTRACT_ID), 2); - } - - { - ContractTestingNOST standardNostromo; - const id standardSeller(1281, 1282, 1283, 1284); - const id standardBidder(1285, 1286, 1287, 1288); - const Asset standardAsset{standardSeller, assetNameFromString("CANSTD")}; - ASSERT_EQ(standardNostromo.issueAsset(standardSeller, standardAsset.assetName, 1), 1); - ASSERT_EQ(standardNostromo.transferShareManagementRightsToNostromo(standardSeller, standardAsset, 1), 1); - const auto standardCreate = standardNostromo.createAuction( - standardSeller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(standardAsset, 1))); - ASSERT_EQ(standardCreate.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ( - standardNostromo.placeBid(standardBidder, standardCreate.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, - NOST::EAuctionError::Success); - EXPECT_EQ(standardNostromo.cancelAuction(standardSeller, standardCreate.auctionIndex, 0).errorCode, - NOST::EAuctionError::AuctionHasAcceptedBid); - EXPECT_EQ(standardNostromo.getAuction(standardCreate.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); - } -} - -TEST(ContractNostromoAuction, CancelAuctionRejectsInvalidCasesWithoutBidsAuction) -{ - ContractTestingNOST nostromo; - const id seller(289, 290, 291, 292); - const id outsider(293, 294, 295, 296); - const uint64 assetName = assetNameFromString("CANIN2"); - const Asset asset{seller, assetName}; - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); - - const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - const auto notFound = nostromo.cancelAuction(seller, 801, 10); - EXPECT_EQ(notFound.errorCode, NOST::EAuctionError::AuctionNotFound); - - const auto forbidden = nostromo.cancelAuction(outsider, createOutput.auctionIndex, 10); - EXPECT_EQ(forbidden.errorCode, NOST::EAuctionError::Forbidden); - - const auto insufficient = nostromo.cancelAuction(seller, createOutput.auctionIndex, 1); - EXPECT_EQ(insufficient.errorCode, NOST::EAuctionError::InsufficientFunds); - - const auto success = nostromo.cancelAuction(seller, createOutput.auctionIndex, 2); - EXPECT_EQ(success.errorCode, NOST::EAuctionError::Success); - - const auto closed = nostromo.cancelAuction(seller, createOutput.auctionIndex, 2); - EXPECT_EQ(closed.errorCode, NOST::EAuctionError::AuctionClosed); -} - -TEST(ContractNostromoAuction, FinalizationArchivesRecordsAndReusesActiveSlotsAuction) -{ - ContractTestingNOST nostromo; - const id seller(481, 482, 483, 484); - const id bidderA(485, 486, 487, 488); - const id bidderB(489, 490, 491, 492); - const Asset asset{seller, assetNameFromString("REUSEA")}; - - EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 2), 2); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); - auto firstInput = - ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE, - NOST_STANDARD_MIN_BID_INCREMENT, NOST_STANDARD_MIN_PRICE); - const auto firstAuction = nostromo.createAuction(seller, firstInput); - ASSERT_EQ(firstAuction.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(bidderA, firstAuction.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, - NOST::EAuctionError::Success); - - EXPECT_EQ(nostromo.stateData().auctionList.population(), 0ULL); - EXPECT_EQ(nostromo.stateData().participantHistoryCounter, 1ULL); - EXPECT_EQ(nostromo.getAuction(firstAuction.auctionIndex).auction.core.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(nostromo.getParticipant(firstAuction.auctionIndex, bidderA).found, 1); - - const auto secondAuction = nostromo.createAuction(seller, firstInput); - ASSERT_EQ(secondAuction.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(bidderB, secondAuction.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, - NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.stateData().participantHistoryCounter, 2ULL); - EXPECT_EQ(nostromo.getAuction(secondAuction.auctionIndex).auction.core.status, NOST::EAuctionStatus::Finalized); -} - -TEST(ContractNostromoAuction, ClosedAuctionHistoryRecordsFinalizedAndCancelledAuctionsAuction) -{ - ContractTestingNOST nostromo; - const id finalizedSeller(501, 502, 503, 504); - const id bidder(505, 506, 507, 508); - const uint64 finalizedAssetName = assetNameFromString("HISFIN"); - const Asset finalizedAsset{finalizedSeller, finalizedAssetName}; - const id cancelledSeller(509, 510, 511, 512); - const uint64 cancelledAssetName = assetNameFromString("HISCAN"); - const Asset cancelledAsset{cancelledSeller, cancelledAssetName}; - - EXPECT_EQ(nostromo.issueAsset(finalizedSeller, finalizedAssetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(finalizedSeller, finalizedAsset, 1), 1); - const auto finalizedCreateOutput = nostromo.createAuction(finalizedSeller, ContractTestingNOST::makeBatchAuctionInput(finalizedAsset, 1, 10)); - ASSERT_EQ(finalizedCreateOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, finalizedCreateOutput.auctionIndex, 1, 10).errorCode, NOST::EAuctionError::Success); - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - - EXPECT_EQ(nostromo.issueAsset(cancelledSeller, cancelledAssetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(cancelledSeller, cancelledAsset, 1), 1); - const auto cancelledCreateOutput = nostromo.createAuction(cancelledSeller, ContractTestingNOST::makeBatchAuctionInput(cancelledAsset, 1, 10)); - ASSERT_EQ(cancelledCreateOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.cancelAuction(cancelledSeller, cancelledCreateOutput.auctionIndex, 1).errorCode, NOST::EAuctionError::Success); - - const auto history = nostromo.getClosedAuctionHistory(); - EXPECT_EQ(history.totalEntries, 2ULL); - EXPECT_TRUE(containsAuctionIndex(history.auctionIndices, history.totalEntries, finalizedCreateOutput.auctionIndex)); - EXPECT_TRUE(containsAuctionIndex(history.auctionIndices, history.totalEntries, cancelledCreateOutput.auctionIndex)); - EXPECT_EQ(nostromo.getAuction(finalizedCreateOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(nostromo.getAuction(cancelledCreateOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Cancelled); -} - -TEST(ContractNostromoAuction, ClosedAuctionHistoryGetterExposesRingBufferOverwriteAuction) -{ - ContractTestingNOST nostromo; - const uint64 overwrittenAuctionIndex = 22000; - const uint64 latestAuctionIndex = 23000; - NOST::AuctionData archivedAuction{}; - - archivedAuction.core.auctionIndex = overwrittenAuctionIndex; - archivedAuction.core.status = NOST::EAuctionStatus::Finalized; - nostromo.stateData().closedAuctionHistory.set(0, archivedAuction); - nostromo.stateData().closedAuctionHistoryCounter = 1; - for (uint64 index = 1; index < NOST_AUCTION_HISTORY_NUM; ++index) - { - archivedAuction.core.auctionIndex = 24000 + index; - nostromo.stateData().closedAuctionHistory.set(index, archivedAuction); - ++nostromo.stateData().closedAuctionHistoryCounter; - } - archivedAuction.core.auctionIndex = latestAuctionIndex; - nostromo.stateData().closedAuctionHistory.set(0, archivedAuction); - ++nostromo.stateData().closedAuctionHistoryCounter; - - const auto history = nostromo.getClosedAuctionHistory(); - EXPECT_EQ(history.totalEntries, NOST_AUCTION_HISTORY_NUM + 1ULL); - EXPECT_FALSE(containsAuctionIndex(history.auctionIndices, history.totalEntries, overwrittenAuctionIndex)); - EXPECT_TRUE(containsAuctionIndex(history.auctionIndices, history.totalEntries, latestAuctionIndex)); - EXPECT_EQ(history.auctionIndices.get(0), latestAuctionIndex); - EXPECT_EQ(nostromo.getAuction(overwrittenAuctionIndex).found, 0); - EXPECT_EQ(nostromo.getAuction(latestAuctionIndex).found, 1); -} -TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) -{ - ContractTestingNOST nostromo; - const id outsider(291, 292, 293, 294); - const id newManagement(295, 296, 297, 298); - - NOST::SetAuctionFees_input coordinatorInput{}; - coordinatorInput.privateAuctionFee = 60000000; - coordinatorInput.publicAuctionCreationFee = 123; - coordinatorInput.auctionCancellationFeeBasisPoints = 900; - coordinatorInput.managementFeeBasisPoints = 60; - coordinatorInput.developmentFeeBasisPoints = 70; - coordinatorInput.takeoverCoordinatorFeeBasisPoints = 80; - coordinatorInput.shareholderDividendBasisPoints = 8500; - coordinatorInput.shareholderFeeBasisPointsTier1 = 400; - coordinatorInput.shareholderFeeBasisPointsTier2 = 350; - coordinatorInput.shareholderFeeBasisPointsTier3 = 300; - coordinatorInput.shareholderFeeBasisPointsTier4 = 250; - const auto defaultFees = nostromo.getAuctionFees(); - - const auto coordinatorForbidden = nostromo.setAuctionFees(outsider, coordinatorInput); - EXPECT_EQ(coordinatorForbidden.errorCode, NOST::EAuctionError::Forbidden); - expectAuctionFeesEqual(nostromo.getAuctionFees(), defaultFees); - - NOST::SetAuctionFees_input invalidCoordinatorInput = coordinatorInput; - invalidCoordinatorInput.privateAuctionFee = -1; - const auto coordinatorInvalid = nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), invalidCoordinatorInput); - EXPECT_EQ(coordinatorInvalid.errorCode, NOST::EAuctionError::InvalidInput); - expectAuctionFeesEqual(nostromo.getAuctionFees(), defaultFees); - - const auto coordinatorSuccess = nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput); - EXPECT_EQ(coordinatorSuccess.errorCode, NOST::EAuctionError::Success); - - auto fees = nostromo.getAuctionFees(); - EXPECT_EQ(fees.privateAuctionFee, 60000000); - EXPECT_EQ(fees.auctionCancellationFeeBasisPoints, 900ULL); - EXPECT_EQ(fees.managementFeeBasisPoints, 60ULL); - EXPECT_EQ(fees.developmentFeeBasisPoints, 70ULL); - EXPECT_EQ(fees.takeoverCoordinatorFeeBasisPoints, 80ULL); - EXPECT_EQ(fees.shareholderDividendBasisPoints, 8500ULL); - EXPECT_EQ(fees.shareholderFeeBasisPointsTier1, 400ULL); - EXPECT_EQ(fees.publicAuctionCreationFee, 123LL); - - const id managementBeforeRejectedUpdates = nostromo.getFeeRecipients().management; - const auto setManagementForbidden = nostromo.setManagement(outsider, newManagement); - EXPECT_EQ(setManagementForbidden.errorCode, NOST::EAuctionError::Forbidden); - EXPECT_EQ(nostromo.getFeeRecipients().management, managementBeforeRejectedUpdates); - - const auto setManagementInvalid = nostromo.setManagement(ContractTestingNOST::takeoverCoordinatorWallet(), NULL_ID); - EXPECT_EQ(setManagementInvalid.errorCode, NOST::EAuctionError::InvalidInput); - EXPECT_EQ(nostromo.getFeeRecipients().management, managementBeforeRejectedUpdates); - - const auto setManagementSuccess = nostromo.setManagement(ContractTestingNOST::takeoverCoordinatorWallet(), newManagement); - EXPECT_EQ(setManagementSuccess.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.getFeeRecipients().management, newManagement); - - NOST::SetAuctionFeesByManagement_input managementInput{}; - managementInput.privateAuctionFee = 70000000; - managementInput.publicAuctionCreationFee = 456; - managementInput.auctionCancellationFeeBasisPoints = 800; - managementInput.managementFeeBasisPoints = 90; - managementInput.developmentFeeBasisPoints = 110; - managementInput.shareholderFeeBasisPointsTier1 = 300; - managementInput.shareholderFeeBasisPointsTier2 = 250; - managementInput.shareholderFeeBasisPointsTier3 = 200; - managementInput.shareholderFeeBasisPointsTier4 = 150; - - const auto coordinatorConfiguredFees = nostromo.getAuctionFees(); - const auto oldManagementForbidden = nostromo.setAuctionFeesByManagement(ContractTestingNOST::managementWallet(), managementInput); - EXPECT_EQ(oldManagementForbidden.errorCode, NOST::EAuctionError::Forbidden); - expectAuctionFeesEqual(nostromo.getAuctionFees(), coordinatorConfiguredFees); - - NOST::SetAuctionFeesByManagement_input invalidManagementInput = managementInput; - invalidManagementInput.managementFeeBasisPoints = 9900; - invalidManagementInput.developmentFeeBasisPoints = 200; - const auto managementInvalid = nostromo.setAuctionFeesByManagement(newManagement, invalidManagementInput); - EXPECT_EQ(managementInvalid.errorCode, NOST::EAuctionError::InvalidInput); - expectAuctionFeesEqual(nostromo.getAuctionFees(), coordinatorConfiguredFees); - - const auto managementSuccess = nostromo.setAuctionFeesByManagement(newManagement, managementInput); - EXPECT_EQ(managementSuccess.errorCode, NOST::EAuctionError::Success); - - fees = nostromo.getAuctionFees(); - EXPECT_EQ(fees.privateAuctionFee, 70000000); - EXPECT_EQ(fees.publicAuctionCreationFee, 456LL); - EXPECT_EQ(fees.auctionCancellationFeeBasisPoints, 800ULL); - EXPECT_EQ(fees.managementFeeBasisPoints, 90ULL); - EXPECT_EQ(fees.developmentFeeBasisPoints, 110ULL); - EXPECT_EQ(fees.takeoverCoordinatorFeeBasisPoints, 80ULL); - EXPECT_EQ(fees.shareholderDividendBasisPoints, 8500ULL); - EXPECT_EQ(fees.shareholderFeeBasisPointsTier1, 300ULL); - EXPECT_EQ(fees.shareholderFeeBasisPointsTier2, 250ULL); - EXPECT_EQ(fees.shareholderFeeBasisPointsTier3, 200ULL); - EXPECT_EQ(fees.shareholderFeeBasisPointsTier4, 150ULL); -} - -TEST(ContractNostromoAuction, BatchSettlementAutomaticallyFlushesLargeSellerPayoutAtEndEpochAuction) -{ - ContractTestingNOST nostromo; - const id seller(901, 902, 903, 904); - const Asset asset{seller, assetNameFromString("BIGPAY")}; - constexpr uint64 bidderCount = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL + 1ULL; - auto feeInput = nostromo.makeCoordinatorFeeInput(0); - feeInput.managementFeeBasisPoints = 0; - feeInput.developmentFeeBasisPoints = 0; - feeInput.takeoverCoordinatorFeeBasisPoints = 0; - feeInput.shareholderFeeBasisPointsTier1 = 0; - feeInput.shareholderFeeBasisPointsTier2 = 0; - feeInput.shareholderFeeBasisPointsTier3 = 0; - feeInput.shareholderFeeBasisPointsTier4 = 0; - ASSERT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), feeInput).errorCode, NOST::EAuctionError::Success); - - EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, static_cast(bidderCount)), static_cast(bidderCount)); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, static_cast(bidderCount)), static_cast(bidderCount)); - const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, bidderCount, 1)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - for (uint64 bidderIndex = 0; bidderIndex < bidderCount; ++bidderIndex) - { - const id bidder(1000 + bidderIndex, 2000 + bidderIndex, 3000 + bidderIndex, 4000 + bidderIndex); - const auto bid = nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, 1, static_cast(MAX_AMOUNT)); - ASSERT_EQ(bid.errorCode, NOST::EAuctionError::Success); - } - - const uint64 grossAmount = bidderCount * static_cast(MAX_AMOUNT); - const sint64 sellerBeforeSettlement = getBalance(seller); - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - - const uint64 expectedImmediatePayout = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL * static_cast(MAX_AMOUNT); - ASSERT_GT(grossAmount, expectedImmediatePayout); - EXPECT_EQ(static_cast(getBalance(seller) - sellerBeforeSettlement), expectedImmediatePayout); - EXPECT_EQ(nostromo.getPendingPayout(seller).amount, grossAmount - expectedImmediatePayout); - EXPECT_EQ(nostromo.getContractStats().stats.totalPendingQuPayouts, grossAmount - expectedImmediatePayout); - - nostromo.endEpoch(); - EXPECT_EQ(static_cast(getBalance(seller) - sellerBeforeSettlement), grossAmount); - EXPECT_EQ(nostromo.getPendingPayout(seller).amount, 0ULL); - EXPECT_EQ(nostromo.getContractStats().stats.totalPendingQuPayouts, 0ULL); -} - -TEST(ContractNostromoAuction, EndEpochPendingPayoutProcessingIsBoundedAuction) -{ - ContractTestingNOST nostromo; - constexpr uint64 recipientCount = NOST_END_EPOCH_PAYOUT_RECIPIENT_NUM + 1ULL; - - nostromo.seedUser(NOST_CONTRACT_ID, static_cast(recipientCount)); - for (uint64 recipientIndex = 0; recipientIndex < recipientCount; ++recipientIndex) - { - const id recipient(12000 + recipientIndex, 13000 + recipientIndex, 14000 + recipientIndex, 15000 + recipientIndex); - nostromo.ensureUser(recipient); - ASSERT_NE(nostromo.stateData().pendingQuPayouts.set(recipient, 1ULL), NULL_INDEX); - nostromo.stateData().totalPendingQuPayouts = sadd(nostromo.stateData().totalPendingQuPayouts, 1ULL); - } - - nostromo.endEpoch(); - EXPECT_EQ(nostromo.stateData().pendingQuPayouts.population(), 1ULL); - EXPECT_EQ(nostromo.getContractStats().stats.totalPendingQuPayouts, 1ULL); - - nostromo.endEpoch(); - EXPECT_EQ(nostromo.stateData().pendingQuPayouts.population(), 0ULL); - EXPECT_EQ(nostromo.getContractStats().stats.totalPendingQuPayouts, 0ULL); -} - -TEST(ContractNostromoAuction, EndEpochDoesNotRematerializeServiceFeesWhenPayoutQueueIsFullAuction) -{ - ContractTestingNOST nostromo; - const id seller(1601, 1602, 1603, 1604); - const id allowedBidder(1605, 1606, 1607, 1608); - const Asset asset{seller, assetNameFromString("FULQUE")}; - - nostromo.setRouteAllFeesToDevelopment(0); - ASSERT_EQ(nostromo.getRouteAllFeesToDevelopment(), 0); - ASSERT_EQ(nostromo.issueAsset(seller, asset.assetName, 1), 1); - ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); - ASSERT_EQ(nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, NOST::EAuctionError::Success); - - constexpr uint64 blockedPayoutAmount = static_cast(MAX_AMOUNT); - for (uint64 recipientIndex = 0; recipientIndex < NOST_PENDING_PAYOUT_NUM; ++recipientIndex) - { - const id recipient(20000 + recipientIndex, 30000 + recipientIndex, 40000 + recipientIndex, 50000 + recipientIndex); - ASSERT_NE(nostromo.stateData().pendingQuPayouts.set(recipient, blockedPayoutAmount), NULL_INDEX); - nostromo.stateData().totalPendingQuPayouts = sadd(nostromo.stateData().totalPendingQuPayouts, blockedPayoutAmount); - } - ASSERT_EQ(nostromo.stateData().pendingQuPayouts.population(), NOST_PENDING_PAYOUT_NUM); - - const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); - const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); - const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); - nostromo.endEpoch(); - - const auto poolAfterFirstEpoch = nostromo.getNostromoFeePool(); - EXPECT_EQ(poolAfterFirstEpoch.feePool.commonServiceFeeAmount, 0ULL); - EXPECT_EQ(poolAfterFirstEpoch.feePool.shareholderDividendAmount, 0ULL); - EXPECT_EQ(poolAfterFirstEpoch.feePool.managementAmount, 4550000ULL); - EXPECT_EQ(poolAfterFirstEpoch.feePool.developmentAmount, 4550000ULL); - EXPECT_EQ(poolAfterFirstEpoch.feePool.takeoverCoordinatorAmount, 4550000ULL); - EXPECT_EQ(poolAfterFirstEpoch.totalAmount, 13650000ULL); - EXPECT_EQ(nostromo.stateData().auctionShareholderDividendPool, 128ULL); - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()), managementBefore); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()), developmentBefore); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()), coordinatorBefore); - - nostromo.endEpoch(); - - const auto poolAfterRetry = nostromo.getNostromoFeePool(); - EXPECT_EQ(poolAfterRetry.feePool.commonServiceFeeAmount, 0ULL); - EXPECT_EQ(poolAfterRetry.feePool.managementAmount, poolAfterFirstEpoch.feePool.managementAmount); - EXPECT_EQ(poolAfterRetry.feePool.developmentAmount, poolAfterFirstEpoch.feePool.developmentAmount); - EXPECT_EQ(poolAfterRetry.feePool.takeoverCoordinatorAmount, poolAfterFirstEpoch.feePool.takeoverCoordinatorAmount); - EXPECT_EQ(poolAfterRetry.totalAmount, poolAfterFirstEpoch.totalAmount); - EXPECT_EQ(nostromo.stateData().auctionShareholderDividendPool, 128ULL); - EXPECT_EQ(nostromo.stateData().pendingQuPayouts.population(), NOST_PENDING_PAYOUT_NUM); -} - -TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) -{ - struct TierCase - { - uint64 grossAmount; - uint64 sellerPayout; - uint64 shareholderDividend; - uint64 managementFee; - uint64 developmentFee; - uint64 coordinatorFee; - uint64 totalFee; - uint64 assetName; - }; - - const TierCase cases[] = { - {5000000000ULL, 4675000000ULL, 225000000ULL, 25000000ULL, 25000000ULL, 50000000ULL, 325000000ULL, assetNameFromString("TIERA1")}, - {5000000001ULL, 4700000001ULL, 202500000ULL, 25000000ULL, 25000000ULL, 47500000ULL, 300000000ULL, assetNameFromString("TIERA2")}, - {50000000001ULL, 47250000001ULL, 1800000000ULL, 250000000ULL, 250000000ULL, 450000000ULL, 2750000000ULL, assetNameFromString("TIERA3")}, - {200000000001ULL, 190000000001ULL, 6300000000ULL, 1000000000ULL, 1000000000ULL, 1700000000ULL, 10000000000ULL, assetNameFromString("TIERA4")}, - }; - - const uint8 routeModes[] = {0, 1}; - for (uint64 caseIndex = 0; caseIndex < sizeof(cases) / sizeof(cases[0]); ++caseIndex) - { - for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) + return 0; + } + uint64 getEpochRevenue() + { + return epochRevenue; + } + void totalRaisedFundChecker(uint32 indexOfFundraising, uint64 raisedFund, uint64 assetName) + { + EXPECT_EQ(raisedFund, fundaraisings.get(indexOfFundraising).raisedFunds); + + if (fundaraisings.get(indexOfFundraising).isCreatedToken) + { + Asset assetInfo; + assetInfo.assetName = assetName; + assetInfo.issuer = id(NOST_CONTRACT_INDEX, 0, 0, 0); + EXPECT_EQ(numberOfShares(assetInfo), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).supplyOfToken); + EXPECT_EQ(numberOfPossessedShares(assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator, projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).supplyOfToken - fundaraisings.get(indexOfFundraising).soldAmount); + } + } + void endEpochSucceedFundraisingChecker(id creator, uint32 indexOfFundraising, uint64 totalInvestedFund, uint64 originalCreatorBalance, uint64 assetName) + { + EXPECT_EQ(numberOfPossessedShares(assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), creator, creator, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).supplyOfToken - div(totalInvestedFund, fundaraisings.get(indexOfFundraising).tokenPrice)); + EXPECT_EQ(fundaraisings.get(indexOfFundraising).raisedFunds, 0); + } + void endEpochFailedFundraisingChecker(uint32 indexOfFundraising) + { + EXPECT_EQ(fundaraisings.get(indexOfFundraising).raisedFunds, 0); + } + void endEpochVoteStatusClearChecker() + { + id userId; + uint64 tierLevel; + uint64 idx = users.nextElementIndex(NULL_INDEX); + uint32 numberOfProject; + Array votedList; + while (idx != NULL_INDEX) { - SCOPED_TRACE(::testing::Message() << "caseIndex=" << caseIndex - << ", routeAllFeesToDevelopment=" << static_cast(routeModes[routeIndex])); - ContractTestingNOST nostromo; - const uint8 routeMode = routeModes[routeIndex]; - const id seller(301 + caseIndex * 2 + routeIndex, 302 + caseIndex * 2 + routeIndex, 303 + caseIndex * 2 + routeIndex, - 304 + caseIndex * 2 + routeIndex); - const id bidder(401 + caseIndex * 2 + routeIndex, 402 + caseIndex * 2 + routeIndex, 403 + caseIndex * 2 + routeIndex, - 404 + caseIndex * 2 + routeIndex); - const Asset asset{seller, cases[caseIndex].assetName}; - nostromo.setRouteAllFeesToDevelopment(routeMode); - EXPECT_EQ(nostromo.issueAsset(seller, cases[caseIndex].assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - - const auto createOutput = nostromo.createAuction( - seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), cases[caseIndex].grossAmount, - cases[caseIndex].grossAmount, NOST_STANDARD_MIN_BID_INCREMENT)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - nostromo.endEpoch(); - const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(cases[caseIndex].shareholderDividend); - const sint64 sellerBefore = getBalance(seller); - const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); - const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); - const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); - const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, cases[caseIndex].grossAmount, cases[caseIndex].grossAmount).errorCode, - NOST::EAuctionError::Success); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - - EXPECT_EQ(getBalance(seller) - sellerBefore, cases[caseIndex].sellerPayout); - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, cases[caseIndex].totalFee); - - const auto pendingFeePool = nostromo.getNostromoFeePool(); - EXPECT_EQ(pendingFeePool.totalAmount, cases[caseIndex].totalFee); - if (routeMode == 0) - { - const uint64 tierAmounts[] = { - pendingFeePool.feePool.shareholderDividendTier1Amount, pendingFeePool.feePool.shareholderDividendTier2Amount, - pendingFeePool.feePool.shareholderDividendTier3Amount, pendingFeePool.feePool.shareholderDividendTier4Amount}; - for (uint64 tierIndex = 0; tierIndex < sizeof(tierAmounts) / sizeof(tierAmounts[0]); ++tierIndex) - { - EXPECT_EQ(tierAmounts[tierIndex], tierIndex == caseIndex ? cases[caseIndex].shareholderDividend : 0ULL); - } - } + userId = users.key(idx); + tierLevel = users.value(idx); - nostromo.endEpoch(); - EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 0ULL); + EXPECT_EQ(voteStatus.get(userId, votedList), 0); + EXPECT_EQ(numberOfVotedProject.get(userId, numberOfProject), 0); - if (routeMode != 0) - { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, cases[caseIndex].totalFee); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); - } - else - { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, cases[caseIndex].managementFee); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, cases[caseIndex].developmentFee); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, cases[caseIndex].coordinatorFee); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); - } + idx = users.nextElementIndex(idx); } - } -} - -TEST(ContractNostromoAuction, FeeReserveGuardTriggersEmergencyPauseOnSuddenDropAuction) -{ - ContractTestingNOST nostromo; - const id seller(1001, 1002, 1003, 1004); - const id bidder(1005, 1006, 1007, 1008); - const Asset asset{seller, assetNameFromString("GRDTRG")}; - - EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 5), 5); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 5), 5); - const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 5, 1)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - auto guardState = nostromo.getFeeReserveGuardState(); - EXPECT_EQ(guardState.isEmergencyPaused, 0); - EXPECT_EQ(guardState.dropBasisPoints, NOST_DEFAULT_FEE_RESERVE_GUARD_DROP_BP); - EXPECT_EQ(guardState.windowSeconds, NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS); - - // Drop the execution fee reserve by 20%, well past the default 10% / 10 minute guard. - const long long reserveBefore = getContractFeeReserve(NOST_CONTRACT_INDEX); - setContractFeeReserve(NOST_CONTRACT_INDEX, reserveBefore - reserveBefore / 5); - nostromo.advanceAndEndTick(1000ULL); - - guardState = nostromo.getFeeReserveGuardState(); - EXPECT_EQ(guardState.isEmergencyPaused, 1); - - const auto stats = nostromo.getContractStats(); - EXPECT_EQ(stats.stats.isEmergencyPaused, 1); - - const id newSeller(1009, 1010, 1011, 1012); - const Asset blockedAsset{newSeller, assetNameFromString("GRDBLK")}; - EXPECT_EQ(nostromo.issueAsset(newSeller, blockedAsset.assetName, 2), 2); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(newSeller, blockedAsset, 2), 2); - nostromo.seedUser(newSeller, 1000); - const sint64 newSellerBefore = getBalance(newSeller); - const auto blockedCreate = nostromo.createAuctionWithFundedReward(newSeller, ContractTestingNOST::makeBatchAuctionInput(blockedAsset, 2, 1), 100); - EXPECT_EQ(blockedCreate.errorCode, NOST::EAuctionError::AuctionPaused); - EXPECT_EQ(getBalance(newSeller), newSellerBefore); - - nostromo.seedUser(bidder, 1000); - const sint64 bidderBefore = getBalance(bidder); - const auto blockedBid = nostromo.placeBidWithFundedReward(bidder, createOutput.auctionIndex, 1, 1, 100); - EXPECT_EQ(blockedBid.errorCode, NOST::EAuctionError::AuctionPaused); - EXPECT_EQ(getBalance(bidder), bidderBefore); - - const sint64 sellerBeforeCancel = getBalance(seller); - const auto blockedCancel = nostromo.cancelAuction(seller, createOutput.auctionIndex, 100); - EXPECT_EQ(blockedCancel.errorCode, NOST::EAuctionError::AuctionPaused); - // cancelAuction() seeds the reward before invoking; a paused call refunds it in full, netting the seeded amount. - EXPECT_EQ(getBalance(seller), sellerBeforeCancel + 100); - - // Non-owners cannot resume the contract. - EXPECT_EQ(nostromo.setEmergencyPause(bidder, false).errorCode, NOST::EAuctionError::Forbidden); - EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 1); - - // The coordinator can manually resume operation. - EXPECT_EQ(nostromo.setEmergencyPause(ContractTestingNOST::takeoverCoordinatorWallet(), false).errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 0); - - nostromo.advanceAndEndTick(1000ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); -} - -TEST(ContractNostromoAuction, FeeReserveGuardResamplesWindowWithoutFalseTriggerAuction) -{ - ContractTestingNOST nostromo; - - const long long baseline = getContractFeeReserve(NOST_CONTRACT_INDEX); - - // A gradual decline spread across multiple guard windows should never trip the single-window drop threshold. - setContractFeeReserve(NOST_CONTRACT_INDEX, baseline - baseline / 20); // -5% - nostromo.advanceAndEndTick((NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS + 10ULL) * 1000ULL); - EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 0); - - const long long afterFirstDrop = getContractFeeReserve(NOST_CONTRACT_INDEX); - setContractFeeReserve(NOST_CONTRACT_INDEX, afterFirstDrop - afterFirstDrop / 20); // another -5% - nostromo.advanceAndEndTick((NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS + 10ULL) * 1000ULL); - EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 0); -} - -TEST(ContractNostromoAuction, SetFeeReserveGuardConfigValidatesAndRestrictsCallerAuction) -{ - ContractTestingNOST nostromo; - const id stranger(1101, 1102, 1103, 1104); - const NOST::GetFeeReserveGuardState_output& defaultGuardState = nostromo.getFeeReserveGuardState(); - - EXPECT_EQ(nostromo.setFeeReserveGuardConfig(stranger, 500ULL, 300ULL).errorCode, NOST::EAuctionError::Forbidden); - - EXPECT_EQ(nostromo.setFeeReserveGuardConfig(ContractTestingNOST::takeoverCoordinatorWallet(), 0ULL, 300ULL).errorCode, - NOST::EAuctionError::InvalidInput); - EXPECT_EQ(nostromo.setFeeReserveGuardConfig(ContractTestingNOST::takeoverCoordinatorWallet(), NOST_BASIS_POINTS_SCALE + 1ULL, 300ULL).errorCode, - NOST::EAuctionError::InvalidInput); - EXPECT_EQ(nostromo.setFeeReserveGuardConfig(ContractTestingNOST::takeoverCoordinatorWallet(), 500ULL, 0ULL).errorCode, - NOST::EAuctionError::InvalidInput); - NOST::GetFeeReserveGuardState_output guardState = nostromo.getFeeReserveGuardState(); - EXPECT_EQ(guardState.dropBasisPoints, defaultGuardState.dropBasisPoints); - EXPECT_EQ(guardState.windowSeconds, defaultGuardState.windowSeconds); - - EXPECT_EQ(nostromo.setFeeReserveGuardConfig(ContractTestingNOST::takeoverCoordinatorWallet(), 500ULL, 300ULL).errorCode, - NOST::EAuctionError::Success); - guardState = nostromo.getFeeReserveGuardState(); - EXPECT_EQ(guardState.dropBasisPoints, 500ULL); - EXPECT_EQ(guardState.windowSeconds, 300ULL); - - EXPECT_EQ(nostromo.setFeeReserveGuardConfig(ContractTestingNOST::managementWallet(), 800ULL, 400ULL).errorCode, NOST::EAuctionError::Success); - guardState = nostromo.getFeeReserveGuardState(); - EXPECT_EQ(guardState.dropBasisPoints, 800ULL); - EXPECT_EQ(guardState.windowSeconds, 400ULL); -} + } + void getStatsChecker(uint64 epochRevenu_t, uint64 totalPoolWeight_t, uint32 numberOfCreatedProject_t, uint32 numberOfFundraising_t, uint32 numberOfRegister_t) + { + EXPECT_EQ(epochRevenu_t, epochRevenue); + EXPECT_EQ(totalPoolWeight_t, totalPoolWeight); + EXPECT_EQ(numberOfCreatedProject_t, numberOfCreatedProject); + EXPECT_EQ(numberOfFundraising_t, numberOfFundraising); + EXPECT_EQ(numberOfRegister_t, numberOfRegister); + } + void removeElementAfterClaimChecker(id user) + { + uint32 tp; + EXPECT_EQ(investors.get(user, tmpInvestedList), 0); + EXPECT_EQ(numberOfInvestedProjects.get(user, tp), 0); + } +}; -TEST(ContractNostromoAuction, EndEpochDistributesPendingFeesWhileEmergencyPausedAuction) +class ContractTestingNostromo : protected ContractTesting { - ContractTestingNOST nostromo; - const id seller(1201, 1202, 1203, 1204); - const Asset asset{seller, assetNameFromString("GRDEPO")}; - - EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 3), 3); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); - const NOST::CreateAuction_output& createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 3, 1)); - ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - - const uint64 poolBefore = nostromo.getPendingServiceFeePool().pendingServiceFeePool; - EXPECT_GT(poolBefore, 0ULL); - - EXPECT_EQ(nostromo.setEmergencyPause(ContractTestingNOST::takeoverCoordinatorWallet(), true).errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 1); - - nostromo.endEpoch(); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, 0ULL); +public: + ContractTestingNostromo() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(NOST); + callSystemProcedure(NOST_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QX); + callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QUOTTERY); + callSystemProcedure(QUOTTERY_CONTRACT_INDEX, INITIALIZE); + } + NostromoChecker* getState() + { + return (NostromoChecker*)contractStates[NOST_CONTRACT_INDEX]; + } + void endEpoch(bool expectSuccess = true) + { + callSystemProcedure(NOST_CONTRACT_INDEX, END_EPOCH, expectSuccess); + } + void registerInTier(const id& registerId, + uint32 tierLevel, + uint64 depositeAmount) + { + NOST::registerInTier_input input; + NOST::registerInTier_output output; + + input.tierLevel = tierLevel; + + invokeUserProcedure(NOST_CONTRACT_INDEX, 1, input, output, registerId, depositeAmount); + } + void logoutFromTier(const id& registerId) + { + NOST::logoutFromTier_input input; + NOST::logoutFromTier_output output; + + invokeUserProcedure(NOST_CONTRACT_INDEX, 2, input, output, registerId, 0); + } + void createProject(const id& registerId, + uint64 tokenName, + uint64 supply, + uint32 startYear, + uint32 startMonth, + uint32 startDay, + uint32 startHour, + uint32 endYear, + uint32 endMonth, + uint32 endDay, + uint32 endHour) + { + NOST::createProject_input input; + NOST::createProject_output output; + + input.tokenName = tokenName; + input.supply = supply; + input.startYear = startYear; + input.startMonth = startMonth; + input.startDay = startDay; + input.startHour = startHour; + input.endYear = endYear; + input.endMonth = endMonth; + input.endDay = endDay; + input.endHour = endHour; + + invokeUserProcedure(NOST_CONTRACT_INDEX, 3, input, output, registerId, NOSTROMO_CREATE_PROJECT_FEE); + } + void voteInProject(const id& registerId, + uint32 indexOfProject, + bit decision) + { + NOST::voteInProject_input input; + NOST::voteInProject_output output; + + input.decision = decision; + input.indexOfProject = indexOfProject; + + invokeUserProcedure(NOST_CONTRACT_INDEX, 4, input, output, registerId, 0); + } + void createFundraising(const id& registerId, + uint64 tokenPrice, + uint64 soldAmount, + uint64 requiredFunds, + + uint32 indexOfProject, + uint32 firstPhaseStartYear, + uint32 firstPhaseStartMonth, + uint32 firstPhaseStartDay, + uint32 firstPhaseStartHour, + uint32 firstPhaseEndYear, + uint32 firstPhaseEndMonth, + uint32 firstPhaseEndDay, + uint32 firstPhaseEndHour, + + uint32 secondPhaseStartYear, + uint32 secondPhaseStartMonth, + uint32 secondPhaseStartDay, + uint32 secondPhaseStartHour, + uint32 secondPhaseEndYear, + uint32 secondPhaseEndMonth, + uint32 secondPhaseEndDay, + uint32 secondPhaseEndHour, + + uint32 thirdPhaseStartYear, + uint32 thirdPhaseStartMonth, + uint32 thirdPhaseStartDay, + uint32 thirdPhaseStartHour, + uint32 thirdPhaseEndYear, + uint32 thirdPhaseEndMonth, + uint32 thirdPhaseEndDay, + uint32 thirdPhaseEndHour, + + uint32 listingStartYear, + uint32 listingStartMonth, + uint32 listingStartDay, + uint32 listingStartHour, + + uint32 cliffEndYear, + uint32 cliffEndMonth, + uint32 cliffEndDay, + uint32 cliffEndHour, + + uint32 vestingEndYear, + uint32 vestingEndMonth, + uint32 vestingEndDay, + uint32 vestingEndHour, + + uint8 threshold, + uint8 TGE, + uint8 stepOfVesting) + { + NOST::createFundraising_input input; + NOST::createFundraising_output output; + + input.tokenPrice = tokenPrice; + input.soldAmount = soldAmount; + input.requiredFunds = requiredFunds; + + input.indexOfProject = indexOfProject; + input.firstPhaseStartYear = firstPhaseStartYear; + input.firstPhaseStartMonth = firstPhaseStartMonth; + input.firstPhaseStartDay = firstPhaseStartDay; + input.firstPhaseStartHour = firstPhaseStartHour; + input.firstPhaseEndYear = firstPhaseEndYear; + input.firstPhaseEndMonth = firstPhaseEndMonth; + input.firstPhaseEndDay = firstPhaseEndDay; + input.firstPhaseEndHour = firstPhaseEndHour; + + input.secondPhaseStartYear = secondPhaseStartYear; + input.secondPhaseStartMonth = secondPhaseStartMonth; + input.secondPhaseStartDay = secondPhaseStartDay; + input.secondPhaseStartHour = secondPhaseStartHour; + input.secondPhaseEndYear = secondPhaseEndYear; + input.secondPhaseEndMonth = secondPhaseEndMonth; + input.secondPhaseEndDay = secondPhaseEndDay; + input.secondPhaseEndHour = secondPhaseEndHour; + + input.thirdPhaseStartYear = thirdPhaseStartYear; + input.thirdPhaseStartMonth = thirdPhaseStartMonth; + input.thirdPhaseStartDay = thirdPhaseStartDay; + input.thirdPhaseStartHour = thirdPhaseStartHour; + input.thirdPhaseEndYear = thirdPhaseEndYear; + input.thirdPhaseEndMonth = thirdPhaseEndMonth; + input.thirdPhaseEndDay = thirdPhaseEndDay; + input.thirdPhaseEndHour = thirdPhaseEndHour; + + input.listingStartYear = listingStartYear; + input.listingStartMonth = listingStartMonth; + input.listingStartDay = listingStartDay; + input.listingStartHour = listingStartHour; + + input.cliffEndYear = cliffEndYear; + input.cliffEndMonth = cliffEndMonth; + input.cliffEndDay = cliffEndDay; + input.cliffEndHour = cliffEndHour; + + input.vestingEndYear = vestingEndYear; + input.vestingEndMonth = vestingEndMonth; + input.vestingEndDay = vestingEndDay; + input.vestingEndHour = vestingEndHour; + + input.threshold = threshold; + input.TGE = TGE; + input.stepOfVesting = stepOfVesting; + + invokeUserProcedure(NOST_CONTRACT_INDEX, 5, input, output, registerId, NOSTROMO_QX_TOKEN_ISSUANCE_FEE); + } + void investInProject(const id& investorId, + uint32 indexOfFundraising, + uint64 investmentAmount) + { + NOST::investInProject_input input; + NOST::investInProject_output output; + + input.indexOfFundraising = indexOfFundraising; + + invokeUserProcedure(NOST_CONTRACT_INDEX, 6, input, output, investorId, investmentAmount); + } + uint64 claimToken(const id& claimerId, + uint64 claimAmount, + uint32 indexOfFundraising) + { + NOST::claimToken_input input; + NOST::claimToken_output output; + + input.amount = claimAmount; + input.indexOfFundraising = indexOfFundraising; + + invokeUserProcedure(NOST_CONTRACT_INDEX, 7, input, output, claimerId, 0); + return output.claimedAmount; + } + void upgradeTier(const id& registerId, + uint32 newTierLevel, + uint64 depositAmount) + { + NOST::upgradeTier_input input; + NOST::upgradeTier_output output; + + input.newTierLevel = newTierLevel; + + invokeUserProcedure(NOST_CONTRACT_INDEX, 8, input, output, registerId, depositAmount); + } + sint64 TransferShareManagementRights(const id& user, Asset asset, sint64 numberOfShares, uint32 newManagingContractIndex) + { + NOST::TransferShareManagementRights_input input; + NOST::TransferShareManagementRights_output output; + + input.asset = asset; + input.newManagingContractIndex = newManagingContractIndex; + input.numberOfShares = numberOfShares; + + invokeUserProcedure(NOST_CONTRACT_INDEX, 9, input, output, user, 100); + + return output.transferredNumberOfShares; + } + NOST::getStats_output getStats() const + { + NOST::getStats_input input; + NOST::getStats_output output; + + callFunction(NOST_CONTRACT_INDEX, 1, input, output); + return output; + } + NOST::getTierLevelByUser_output getTierLevelByUser(const id& registerId) const + { + NOST::getTierLevelByUser_input input; + NOST::getTierLevelByUser_output output; + + input.userId = registerId; + callFunction(NOST_CONTRACT_INDEX, 2, input, output); + return output; + } + NOST::getUserVoteStatus_output getUserVoteStatus(const id& registerId) const + { + NOST::getUserVoteStatus_input input; + NOST::getUserVoteStatus_output output; + + input.userId = registerId; + callFunction(NOST_CONTRACT_INDEX, 3, input, output); + return output; + } + NOST::checkTokenCreatability_output checkTokenCreatability(uint64 tokenName) const + { + NOST::checkTokenCreatability_input input; + NOST::checkTokenCreatability_output output; + + input.tokenName = tokenName; + callFunction(NOST_CONTRACT_INDEX, 4, input, output); + return output; + } + NOST::getNumberOfInvestedProjects_output getNumberOfInvestedProjects(const id& invsetorId) const + { + NOST::getNumberOfInvestedProjects_input input; + NOST::getNumberOfInvestedProjects_output output; + + input.userId = invsetorId; + callFunction(NOST_CONTRACT_INDEX, 5, input, output); + return output; + } + NOST::getProjectByIndex_output getProjectByIndex(uint32 indexOfProject) const + { + NOST::getProjectByIndex_input input; + NOST::getProjectByIndex_output output; + + input.indexOfProject = indexOfProject; + callFunction(NOST_CONTRACT_INDEX, 6, input, output); + return output; + } + NOST::getFundarasingByIndex_output getFundarasingByIndex(uint32 indexOfFundraising) const + { + NOST::getFundarasingByIndex_input input; + NOST::getFundarasingByIndex_output output; + + input.indexOfFundarasing = indexOfFundraising; + callFunction(NOST_CONTRACT_INDEX, 7, input, output); + return output; + } + NOST::getProjectIndexListByCreator_output getProjectIndexListByCreator(const id& creatorId) const + { + NOST::getProjectIndexListByCreator_input input; + NOST::getProjectIndexListByCreator_output output; + + input.creator = creatorId; + callFunction(NOST_CONTRACT_INDEX, 8, input, output); + return output; + } + NOST::getInfoUserInvested_output getInfoUserInvested(const id& investorId) const + { + NOST::getInfoUserInvested_input input; + NOST::getInfoUserInvested_output output; + + input.investorId = investorId; + callFunction(NOST_CONTRACT_INDEX, 9, input, output); + return output; + } + uint64 getMaxClaimAmount(const id& investorId, uint32 indexOfFundraising) const + { + NOST::getMaxClaimAmount_input input; + NOST::getMaxClaimAmount_output output; + + input.investorId = investorId; + input.indexOfFundraising = indexOfFundraising; + callFunction(NOST_CONTRACT_INDEX, 10, input, output); + return output.amount; + } +}; - // The auction should remain paused after END_EPOCH; only a manual resume clears it. - EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 1); +TEST(TestContractNostromo, registerAndLogoutAndUpgradeFromTierChecker) +{ + ContractTestingNostromo nostromoTestCaseA; + + std::map duplicatedUser; + auto registers = getRandomUsers(10000, 10000); + + uint32 countOfRegister = 0, totalPoolWeight = 0; + uint64 totalDepositedQubic = 0, totalLogoutFeeAmount = 0; + + for (const auto& user : registers) + { + if (duplicatedUser[user]) + { + continue; + } + uint32 tierLevel = (uint32)random(1, 5); + uint64 depositeAmount, upgradeDeltaDepositeAmount; + switch (tierLevel) + { + case 1: + depositeAmount = NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT; + upgradeDeltaDepositeAmount = NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT - NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT; + totalLogoutFeeAmount += NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT * NOSTROMO_TIER_CHESTBURST_UNSTAKE_FEE / 100; + totalPoolWeight += NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; + break; + case 2: + depositeAmount = NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT; + upgradeDeltaDepositeAmount = NOSTROMO_TIER_DOG_STAKE_AMOUNT - NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT; + totalLogoutFeeAmount += NOSTROMO_TIER_DOG_STAKE_AMOUNT * NOSTROMO_TIER_DOG_UNSTAKE_FEE / 100; + totalPoolWeight += NOSTROMO_TIER_DOG_POOL_WEIGHT; + break; + case 3: + depositeAmount = NOSTROMO_TIER_DOG_STAKE_AMOUNT; + upgradeDeltaDepositeAmount = NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT - NOSTROMO_TIER_DOG_STAKE_AMOUNT; + totalLogoutFeeAmount += NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT * NOSTROMO_TIER_XENOMORPH_UNSTAKE_FEE / 100; + totalPoolWeight += NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; + break; + case 4: + depositeAmount = NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT; + upgradeDeltaDepositeAmount = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT - NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT; + totalLogoutFeeAmount += NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT * NOSTROMO_TIER_WARRIOR_UNSTAKE_FEE / 100; + totalPoolWeight += NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; + break; + case 5: + depositeAmount = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT; + totalLogoutFeeAmount += NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT * NOSTROMO_TIER_WARRIOR_UNSTAKE_FEE / 100; + totalPoolWeight += NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; + break; + default: + break; + } + // Register Tier + totalDepositedQubic += depositeAmount; + increaseEnergy(user, depositeAmount); + nostromoTestCaseA.registerInTier(user, tierLevel, depositeAmount); + nostromoTestCaseA.getState()->registerChecker(user, tierLevel, countOfRegister); + // Upgrade Tier + totalDepositedQubic += upgradeDeltaDepositeAmount; + increaseEnergy(user, upgradeDeltaDepositeAmount); + nostromoTestCaseA.upgradeTier(user, tierLevel + 1, upgradeDeltaDepositeAmount); + + if (tierLevel == 5) + { + nostromoTestCaseA.getState()->registerChecker(user, tierLevel, countOfRegister); + } + else + { + nostromoTestCaseA.getState()->registerChecker(user, tierLevel + 1, countOfRegister); + } + + duplicatedUser[user] = 1; + countOfRegister++; + } + nostromoTestCaseA.getState()->countOfRegisterChecker(countOfRegister); + nostromoTestCaseA.getState()->epochRevenueChecker(0); + nostromoTestCaseA.getState()->totalPoolWeightChecker(totalPoolWeight); + EXPECT_EQ(totalDepositedQubic, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); + + duplicatedUser.clear(); + for (const auto& user : registers) + { + if (duplicatedUser[user]) + { + continue; + } + // Logout From Tier + nostromoTestCaseA.logoutFromTier(user); + duplicatedUser[user] = 1; + nostromoTestCaseA.getState()->logoutFromTierChecker(user); + } + EXPECT_EQ(totalLogoutFeeAmount, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); + nostromoTestCaseA.getState()->countOfRegisterChecker(0); + nostromoTestCaseA.getState()->epochRevenueChecker(totalLogoutFeeAmount); + nostromoTestCaseA.getState()->totalPoolWeightChecker(0); +} + +TEST(TestContractNostromo, createProjectAndVoteInProjectChecker) +{ + ContractTestingNostromo nostromoTestCaseB; + + auto registers = getRandomUsers(1000, 1000); + + // Register in each Tiers + increaseEnergy(registers[0], NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE); + nostromoTestCaseB.registerInTier(registers[0], 1, NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT); + + increaseEnergy(registers[1], NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE); + nostromoTestCaseB.registerInTier(registers[1], 2, NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT); + + increaseEnergy(registers[2], NOSTROMO_TIER_DOG_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE); + nostromoTestCaseB.registerInTier(registers[2], 3, NOSTROMO_TIER_DOG_STAKE_AMOUNT); + + increaseEnergy(registers[3], NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE); + nostromoTestCaseB.registerInTier(registers[3], 4, NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT); + + increaseEnergy(registers[4], NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE); + nostromoTestCaseB.registerInTier(registers[4], 5, NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT); + + setMemory(utcTime, 0); + utcTime.Year = 2025; + utcTime.Month = 6; + utcTime.Day = 12; + utcTime.Hour = 0; + updateQpiTime(); + + uint64 assetName = assetNameFromString("AAAA"); + + // This creation should be failed because there is no qualified to create the project. + nostromoTestCaseB.createProject(registers[0], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); + nostromoTestCaseB.getState()->numberOfCreatedProjectChecker(0); + nostromoTestCaseB.getState()->epochRevenueChecker(0); + EXPECT_EQ(getBalance(registers[0]), NOSTROMO_CREATE_PROJECT_FEE); + + // This creation should be failed because there is no qualified to create the project. + assetName = assetNameFromString("BBBB"); + nostromoTestCaseB.createProject(registers[1], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); + nostromoTestCaseB.getState()->numberOfCreatedProjectChecker(0); + nostromoTestCaseB.getState()->epochRevenueChecker(0); + EXPECT_EQ(getBalance(registers[1]), NOSTROMO_CREATE_PROJECT_FEE); + + // This creation should be failed because there is no qualified to create the project. + assetName = assetNameFromString("CCCC"); + nostromoTestCaseB.createProject(registers[2], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); + nostromoTestCaseB.getState()->numberOfCreatedProjectChecker(0); + nostromoTestCaseB.getState()->epochRevenueChecker(0); + EXPECT_EQ(getBalance(registers[2]), NOSTROMO_CREATE_PROJECT_FEE); + + + //This creation should be succeed because there is a qualified to create the project. + assetName = assetNameFromString("DDDD"); + nostromoTestCaseB.createProject(registers[3], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); + nostromoTestCaseB.getState()->numberOfCreatedProjectChecker(1); + nostromoTestCaseB.getState()->epochRevenueChecker(NOSTROMO_CREATE_PROJECT_FEE); + nostromoTestCaseB.getState()->createdProjectChecker(0, registers[3], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); + EXPECT_EQ(getBalance(registers[3]), 0); + + // This creation should be succeed because there is a qualified to create the project. + assetName = assetNameFromString("EEEE"); + nostromoTestCaseB.createProject(registers[4], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); + nostromoTestCaseB.getState()->numberOfCreatedProjectChecker(2); + nostromoTestCaseB.getState()->epochRevenueChecker(NOSTROMO_CREATE_PROJECT_FEE * 2); + nostromoTestCaseB.getState()->createdProjectChecker(1, registers[4], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); + EXPECT_EQ(getBalance(registers[4]), 0); + + // checkTokenCreatability function checker + EXPECT_EQ(nostromoTestCaseB.checkTokenCreatability(assetName).result, 1); + assetName = assetNameFromString("ABCD"); + EXPECT_EQ(nostromoTestCaseB.checkTokenCreatability(assetName).result, 0); + + setMemory(utcTime, 0); + utcTime.Year = 2025; + utcTime.Month = 6; + utcTime.Day = 13; + utcTime.Hour = 0; + updateQpiTime(); + + Array votedList; + + nostromoTestCaseB.voteInProject(registers[0], 0, 0); + votedList.set(0, 0); + nostromoTestCaseB.voteInProject(registers[1], 0, 1); + nostromoTestCaseB.voteInProject(registers[2], 0, 1); + nostromoTestCaseB.voteInProject(registers[3], 0, 1); + nostromoTestCaseB.voteInProject(registers[4], 0, 0); + + nostromoTestCaseB.getState()->voteInProjectChecker(0, 3, 2); + nostromoTestCaseB.getState()->numberOfVotedProjectAndVotedListChecker(registers[0], 1, votedList); + + // This vote should be failed. + nostromoTestCaseB.voteInProject(registers[0], 0, 0); + nostromoTestCaseB.getState()->voteInProjectChecker(0, 3, 2); + nostromoTestCaseB.getState()->numberOfVotedProjectAndVotedListChecker(registers[0], 1, votedList); + + // This vote should be succeed. + nostromoTestCaseB.voteInProject(registers[0], 1, 0); + votedList.set(1, 1); + nostromoTestCaseB.getState()->voteInProjectChecker(1, 0, 1); + nostromoTestCaseB.getState()->numberOfVotedProjectAndVotedListChecker(registers[0], 2, votedList); + + nostromoTestCaseB.voteInProject(registers[1], 1, 1); + nostromoTestCaseB.voteInProject(registers[2], 1, 1); + nostromoTestCaseB.voteInProject(registers[3], 1, 1); + nostromoTestCaseB.voteInProject(registers[4], 1, 1); + nostromoTestCaseB.getState()->voteInProjectChecker(1, 4, 1); +} + +TEST(TestContractNostromo, createFundraisingAndInvestInProjectAndClaimTokenChecker) +{ + uint64 epochRevenu_t = 0; + uint32 numberOfCreatedProject_t = 0; + uint32 numberOfFundraising_t = 0;; + + ContractTestingNostromo nostromoTestCaseC; + + auto registers = getRandomUsers(10000, 10000); + + setMemory(utcTime, 0); + utcTime.Year = 2025; + utcTime.Month = 6; + utcTime.Day = 11; + utcTime.Hour = 0; + updateQpiTime(); + + increaseEnergy(registers[0], NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE + NOSTROMO_QX_TOKEN_ISSUANCE_FEE); + nostromoTestCaseC.registerInTier(registers[0], 5, NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT); + uint64 assetName = assetNameFromString("GGGG"); + nostromoTestCaseC.createProject(registers[0], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); + + // getProjectByIndex function Checker + NOST::getProjectByIndex_output getProjectByIndex_output = nostromoTestCaseC.getProjectByIndex(0); + + EXPECT_EQ(getProjectByIndex_output.project.creator, registers[0]); + uint32 tmpDate; + NOST::packNostromoDate(25, 6, 15, 0, 0, 0, tmpDate); + EXPECT_EQ(getProjectByIndex_output.project.endDate , tmpDate); + EXPECT_EQ(getProjectByIndex_output.project.isCreatedFundarasing , 0); + EXPECT_EQ(getProjectByIndex_output.project.numberOfNo, 0); + EXPECT_EQ(getProjectByIndex_output.project.numberOfYes, 0); + NOST::packNostromoDate(25, 6, 13, 0, 0, 0, tmpDate); + EXPECT_EQ(getProjectByIndex_output.project.startDate, tmpDate); + EXPECT_EQ(getProjectByIndex_output.project.supplyOfToken, 21000000); + EXPECT_EQ(getProjectByIndex_output.project.tokenName, assetName); + + numberOfCreatedProject_t++; + epochRevenu_t += 100000000; + + std::map duplicatedUser; + uint64 totalPoolWeight = NOSTROMO_TIER_WARRIOR_POOL_WEIGHT, totalDepositedQubic = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT; + uint32 countOfRegister = 0; + + for (const auto& user : registers) + { + if (countOfRegister == 0) + { + countOfRegister++; + continue; + } + + if (duplicatedUser[user]) + { + continue; + } + uint8 tierLevel = (uint8)random(1, 5); + uint64 depositeAmount, userPoolWeight; + switch (tierLevel) + { + case 1: + depositeAmount = NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT; + totalPoolWeight += NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT; + userPoolWeight = NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT; + break; + case 2: + depositeAmount = NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT; + totalPoolWeight += NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; + userPoolWeight = NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; + break; + case 3: + depositeAmount = NOSTROMO_TIER_DOG_STAKE_AMOUNT; + totalPoolWeight += NOSTROMO_TIER_DOG_POOL_WEIGHT; + userPoolWeight = NOSTROMO_TIER_DOG_POOL_WEIGHT; + break; + case 4: + depositeAmount = NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT; + totalPoolWeight += NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; + userPoolWeight = NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; + break; + case 5: + depositeAmount = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT; + totalPoolWeight += NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; + userPoolWeight = NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; + break; + default: + break; + } + + // Register Tier + totalDepositedQubic += depositeAmount; + increaseEnergy(user, depositeAmount); + nostromoTestCaseC.registerInTier(user, tierLevel, depositeAmount); + + duplicatedUser[user] = 1; + countOfRegister++; + + // getTierLevelByUser function Checker + EXPECT_EQ(nostromoTestCaseC.getTierLevelByUser(user).tierLevel, tierLevel); + } + + // Vote in Project + utcTime.Year = 2025; + utcTime.Month = 6; + utcTime.Day = 14; + utcTime.Hour = 0; + updateQpiTime(); + + uint32 Ynumber = 0, Nnumber = 0; + duplicatedUser.clear(); + + for (const auto& user : registers) + { + if (duplicatedUser[user]) + { + continue; + } + + bit decision = (bit)random(0, 3); + if (decision) + { + Ynumber++; + } + else + { + Nnumber++; + } + + nostromoTestCaseC.voteInProject(user, 0, decision); + duplicatedUser[user] = 1; + } + nostromoTestCaseC.getState()->voteInProjectChecker(0, Ynumber, Nnumber); + + // Create the Fundraising + // This fundraising should not be created because the voting is not finished yet. + utcTime.Year = 2025; + utcTime.Month = 6; + utcTime.Day = 14; + utcTime.Hour = 0; + updateQpiTime(); + + nostromoTestCaseC.createFundraising(registers[0], 100, 2000000, 150000000, 0, + 25, 6, 17, 0, + 25, 6, 25, 0, + 25, 6, 28, 0, + 25, 7, 1, 0, + 25, 7, 10, 0, + 25, 7, 15, 0, + 25, 7, 25, 0, + 25, 7, 27, 0, + 26, 7, 27, 0, + 20, 10, 12); + + nostromoTestCaseC.getState()->countOfFundraisingChecker(0); + + // It should be created. + + utcTime.Year = 2025; + utcTime.Month = 6; + utcTime.Day = 16; + utcTime.Hour = 0; + updateQpiTime(); + + nostromoTestCaseC.createFundraising(registers[0], 100000, 2000000, 150000000000, 0, + 25, 6, 17, 0, + 25, 6, 25, 0, + 25, 6, 28, 0, + 25, 7, 1, 0, + 25, 7, 10, 0, + 25, 7, 15, 0, + 25, 7, 25, 0, + 25, 7, 27, 0, + 26, 7, 27, 0, + 20, 10, 12); + numberOfFundraising_t++; + + nostromoTestCaseC.getState()->countOfFundraisingChecker(1); + nostromoTestCaseC.getState()->createFundraisingChecker(registers[0], 100000, 2000000, 150000000000, 0, + 25, 6, 17, 0, + 25, 6, 25, 0, + 25, 6, 28, 0, + 25, 7, 1, 0, + 25, 7, 10, 0, + 25, 7, 15, 0, + 25, 7, 25, 0, + 25, 7, 27, 0, + 26, 7, 27, 0, + 20, 10, 12, 0); + + // getFundarasingByIndex function checker + NOST::getFundarasingByIndex_output getFundarasingByIndex_output = nostromoTestCaseC.getFundarasingByIndex(0); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.indexOfProject, 0); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.isCreatedToken, 0); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.raisedFunds, 0); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.requiredFunds, 150000000000); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.soldAmount, 2000000); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.stepOfVesting, 12); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.TGE, 10); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.threshold, 20); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.tokenPrice, 100000); + NOST::packNostromoDate(25, 6, 17, 0, 0, 0, tmpDate); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.firstPhaseStartDate, tmpDate); + NOST::packNostromoDate(25, 6, 25, 0, 0, 0, tmpDate); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.firstPhaseEndDate, tmpDate); + NOST::packNostromoDate(25, 6, 28, 0, 0, 0, tmpDate); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.secondPhaseStartDate, tmpDate); + NOST::packNostromoDate(25, 7, 1, 0, 0, 0, tmpDate); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.secondPhaseEndDate, tmpDate); + NOST::packNostromoDate(25, 7, 10, 0, 0, 0, tmpDate); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.thirdPhaseStartDate, tmpDate); + NOST::packNostromoDate(25, 7, 15, 0, 0, 0, tmpDate); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.thirdPhaseEndDate, tmpDate); + NOST::packNostromoDate(25, 7, 25, 0, 0, 0, tmpDate); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.listingStartDate, tmpDate); + NOST::packNostromoDate(25, 7, 27, 0, 0, 0, tmpDate); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.cliffEndDate, tmpDate); + NOST::packNostromoDate(26, 7, 27, 0, 0, 0, tmpDate); + EXPECT_EQ(getFundarasingByIndex_output.fundarasing.vestingEndDate, tmpDate); + + // Phase 1 Investment + utcTime.Year = 2025; + utcTime.Month = 6; + utcTime.Day = 17; + utcTime.Hour = 1; + updateQpiTime(); + + uint64 facehuggerMaxInvestAmount = 180000000000 * NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT / totalPoolWeight; + uint64 chestburstMaxInvestAmount = 180000000000 * NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT / totalPoolWeight; + uint64 dogMaxInvestAmount = 180000000000 * NOSTROMO_TIER_DOG_POOL_WEIGHT / totalPoolWeight; + uint64 xenomorphMaxInvestAmount = 180000000000 * NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT / totalPoolWeight; + uint64 warriorMaxInvestAmount = 180000000000 * NOSTROMO_TIER_WARRIOR_POOL_WEIGHT / totalPoolWeight; + + uint64 totalInvestedAmount = 0; + duplicatedUser.clear(); + uint32 ct = 0; + uint32 overDeposit = 1000; // it should be ignored + uint64 originalSCBalance = getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0)); + + std::map investedAmountMP; + for (const auto& user : registers) + { + if (duplicatedUser[user]) + { + ct++; + continue; + } + ct++; + increaseEnergy(user, 180000000000); + uint8 tierLevel = nostromoTestCaseC.getState()->getTierLevel(user); + + if (ct >= 4000) + { + // Phase 2 Investment + utcTime.Year = 2025; + utcTime.Month = 6; + utcTime.Day = 29; + utcTime.Hour = 0; + updateQpiTime(); + } + + switch (tierLevel) + { + case 1: + if (ct < 4000) + { + totalInvestedAmount += facehuggerMaxInvestAmount; + investedAmountMP[user] += facehuggerMaxInvestAmount; + } + nostromoTestCaseC.investInProject(user, 0, facehuggerMaxInvestAmount + overDeposit); + break; + case 2: + if (ct < 4000) + { + totalInvestedAmount += chestburstMaxInvestAmount; + investedAmountMP[user] += chestburstMaxInvestAmount; + } + nostromoTestCaseC.investInProject(user, 0, chestburstMaxInvestAmount + overDeposit); + break; + case 3: + if (ct < 4000) + { + totalInvestedAmount += dogMaxInvestAmount; + investedAmountMP[user] += dogMaxInvestAmount; + } + nostromoTestCaseC.investInProject(user, 0, dogMaxInvestAmount + overDeposit); + break; + case 4: + totalInvestedAmount += xenomorphMaxInvestAmount; + investedAmountMP[user] += xenomorphMaxInvestAmount; + nostromoTestCaseC.investInProject(user, 0, xenomorphMaxInvestAmount + overDeposit); + break; + case 5: + totalInvestedAmount += warriorMaxInvestAmount; + investedAmountMP[user] += warriorMaxInvestAmount; + nostromoTestCaseC.investInProject(user, 0, warriorMaxInvestAmount + overDeposit); + break; + + default: + break; + } + + duplicatedUser[user] = 1; + } + + nostromoTestCaseC.getState()->totalRaisedFundChecker(0, totalInvestedAmount, assetName); + EXPECT_EQ(originalSCBalance + totalInvestedAmount - NOSTROMO_QX_TOKEN_ISSUANCE_FEE, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); + + // Phase 3 Investment + utcTime.Year = 2025; + utcTime.Month = 7; + utcTime.Day = 11; + utcTime.Hour = 0; + updateQpiTime(); + + uint64 amount = 10000000; + duplicatedUser.clear(); + ct = 0; + for (const auto& user : registers) + { + if (duplicatedUser[user]) + { + continue; + } + ct++; + uint8 tierLevel = nostromoTestCaseC.getState()->getTierLevel(user); + increaseEnergy(user, amount); + nostromoTestCaseC.investInProject(user, 0, amount); + if (totalInvestedAmount + amount < 180000000000) + { + totalInvestedAmount += amount; + investedAmountMP[user] += amount; + + // getNumberOfInvestedProjects function checker + NOST::getNumberOfInvestedProjects_output getNumberOfInvestedProjects_output = nostromoTestCaseC.getNumberOfInvestedProjects(user); + + EXPECT_EQ(getNumberOfInvestedProjects_output.numberOfInvestedProjects, 1); + } + duplicatedUser[user] = 1; + } + + nostromoTestCaseC.getState()->totalRaisedFundChecker(0, totalInvestedAmount, assetName); + EXPECT_EQ(originalSCBalance + totalInvestedAmount - NOSTROMO_QX_TOKEN_ISSUANCE_FEE, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); + + // getMaxClaimAmount function checker + utcTime.Year = 2025; + utcTime.Month = 7; + utcTime.Day = 26; + utcTime.Hour = 0; + updateQpiTime(); + + duplicatedUser.clear(); + for (const auto& user : registers) + { + if (duplicatedUser[user]) + { + continue; + } + + EXPECT_EQ(nostromoTestCaseC.getMaxClaimAmount(user, 0), investedAmountMP[user] / 100000 * 10 / 100); + + duplicatedUser[user] = 1; + } + + utcTime.Year = 2025; + utcTime.Month = 8; + utcTime.Day = 5; + utcTime.Hour = 0; + updateQpiTime(); + + duplicatedUser.clear(); + for (const auto& user : registers) + { + if (duplicatedUser[user]) + { + continue; + } + + EXPECT_EQ(nostromoTestCaseC.getMaxClaimAmount(user, 0), investedAmountMP[user] / 100000 * (10 + 7) / 100); + + duplicatedUser[user] = 1; + } + + utcTime.Year = 2026; + utcTime.Month = 8; + utcTime.Day = 5; + utcTime.Hour = 0; + updateQpiTime(); + + duplicatedUser.clear(); + for (const auto& user : registers) + { + if (duplicatedUser[user]) + { + continue; + } + + EXPECT_EQ(nostromoTestCaseC.getMaxClaimAmount(user, 0), investedAmountMP[user] / 100000); + + duplicatedUser[user] = 1; + } + + // claimToken Checker + std::map claimedAmountMP; + for (uint32 i = 1; i <= 12; i++) + { + if (i >= 6) + { + utcTime.Year = 2026; + } + utcTime.Month = (7 + i) % 12; + if (utcTime.Month == 0) utcTime.Month = 12; + utcTime.Day = 5; + utcTime.Hour = 0; + updateQpiTime(); + + duplicatedUser.clear(); + for (const auto& user : registers) + { + if (duplicatedUser[user]) + { + continue; + } + + uint64 investedAmount = nostromoTestCaseC.getState()->getInvestedAmount(0, user); + uint64 claimAmount = investedAmount / 100000 / 12; + claimedAmountMP[user] += nostromoTestCaseC.claimToken(user, claimAmount, 0); + + duplicatedUser[user] = 1; + } + } + + // getInfoUserInvested function checker + duplicatedUser.clear(); + for (const auto& user : registers) + { + if (duplicatedUser[user]) + { + continue; + } + + duplicatedUser[user] = 1; + + NOST::getInfoUserInvested_output getInfoUserInvested_output = nostromoTestCaseC.getInfoUserInvested(user); + EXPECT_EQ(getInfoUserInvested_output.listUserInvested.get(0).indexOfFundraising, 0); + EXPECT_EQ(getInfoUserInvested_output.listUserInvested.get(0).investedAmount, investedAmountMP[user]); + EXPECT_EQ(getInfoUserInvested_output.listUserInvested.get(0).claimedAmount, claimedAmountMP[user]); + } + + // Checking to remove element after claiming the max amount + utcTime.Year = 2026; + utcTime.Month = 8; + utcTime.Day = 5; + utcTime.Hour = 0; + updateQpiTime(); + + duplicatedUser.clear(); + for (const auto& user : registers) + { + if (duplicatedUser[user]) + { + continue; + } + uint64 claimAmount = nostromoTestCaseC.getMaxClaimAmount(user, 0) - claimedAmountMP[user]; + claimedAmountMP[user] += nostromoTestCaseC.claimToken(user, claimAmount, 0); + + duplicatedUser[user] = 1; + + nostromoTestCaseC.getState()->removeElementAfterClaimChecker(user); + } + + ct = 0; + duplicatedUser.clear(); + for (const auto& user : registers) + { + if (duplicatedUser[user]) + { + continue; + } + if (ct == 0) + { + EXPECT_EQ(numberOfPossessedShares(assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), user, user, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX) - 19000000, claimedAmountMP[user]); + } + else + { + EXPECT_EQ(numberOfPossessedShares(assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), user, user, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX), claimedAmountMP[user]); + } + ct++; + duplicatedUser[user] = 1; + } + + // transferShareManagementRights Checker + increaseEnergy(registers[0], 1000000); + + Asset asset; + asset.assetName = assetName; + asset.issuer = id(NOST_CONTRACT_INDEX, 0, 0, 0); + EXPECT_EQ(nostromoTestCaseC.TransferShareManagementRights(registers[0], asset, 10000, QX_CONTRACT_INDEX), 10000); + EXPECT_EQ(numberOfPossessedShares(asset.assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), registers[0], registers[0], QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), 10000); + + // EndEpochSucceedFundraising Checker + utcTime.Year = 2025; + utcTime.Month = 6; + utcTime.Day = 20; + utcTime.Hour = 0; + updateQpiTime(); + + increaseEnergy(registers[0], NOSTROMO_CREATE_PROJECT_FEE); + assetName = assetNameFromString("AAAA"); + nostromoTestCaseC.createProject(registers[0], assetName, 21000000, 25, 6, 22, 0, 25, 6, 25, 0); + numberOfCreatedProject_t++; + epochRevenu_t += 100000000; + + utcTime.Year = 2025; + utcTime.Month = 6; + utcTime.Day = 23; + utcTime.Hour = 0; + updateQpiTime(); + + Ynumber = 0; Nnumber = 0; + duplicatedUser.clear(); + + for (const auto& user : registers) + { + if (duplicatedUser[user]) + { + continue; + } + + bit decision = (bit)random(0, 3); + if (decision) + { + Ynumber++; + } + else + { + Nnumber++; + } + + nostromoTestCaseC.voteInProject(user, 1, decision); + duplicatedUser[user] = 1; + + // getUserVoteStatus function Checker + NOST::getUserVoteStatus_output getUserVoteStatus_output = nostromoTestCaseC.getUserVoteStatus(user); + EXPECT_EQ(getUserVoteStatus_output.numberOfVotedProjects, 2); + EXPECT_EQ(getUserVoteStatus_output.projectIndexList.get(0), 0); + EXPECT_EQ(getUserVoteStatus_output.projectIndexList.get(1), 1); + } + nostromoTestCaseC.getState()->voteInProjectChecker(1, Ynumber, Nnumber); + + utcTime.Year = 2025; + utcTime.Month = 6; + utcTime.Day = 26; + utcTime.Hour = 0; + updateQpiTime(); + increaseEnergy(registers[0], NOSTROMO_QX_TOKEN_ISSUANCE_FEE); + + nostromoTestCaseC.createFundraising(registers[0], 100000, 2000000, 150000000000, 1, + 25, 6, 27, 0, + 25, 7, 5, 0, + 25, 7, 8, 0, + 25, 7, 10, 0, + 25, 7, 20, 0, + 25, 7, 23, 0, + 25, 7, 25, 0, + 25, 7, 27, 0, + 26, 7, 27, 0, + 20, 10, 12); + numberOfFundraising_t++; + + nostromoTestCaseC.getState()->countOfFundraisingChecker(2); + nostromoTestCaseC.getState()->createFundraisingChecker(registers[0], 100000, 2000000, 150000000000, 1, + 25, 6, 27, 0, + 25, 7, 5, 0, + 25, 7, 8, 0, + 25, 7, 10, 0, + 25, 7, 20, 0, + 25, 7, 23, 0, + 25, 7, 25, 0, + 25, 7, 27, 0, + 26, 7, 27, 0, + 20, 10, 12, 1); + + utcTime.Year = 2025; + utcTime.Month = 6; + utcTime.Day = 27; + utcTime.Hour = 1; + updateQpiTime(); + + uint64 totalInvestedAmount_2 = 0; + duplicatedUser.clear(); + ct = 0; + originalSCBalance = getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0)); + for (const auto& user : registers) + { + if (duplicatedUser[user]) + { + ct++; + continue; + } + ct++; + increaseEnergy(user, 180000000000); + uint8 tierLevel = nostromoTestCaseC.getState()->getTierLevel(user); + + if (ct >= 4000) + { + + // Phase 2 Investment + utcTime.Year = 2025; + utcTime.Month = 7; + utcTime.Day = 9; + utcTime.Hour = 0; + updateQpiTime(); + } + + switch (tierLevel) + { + case 1: + if (ct < 4000) + { + totalInvestedAmount_2 += facehuggerMaxInvestAmount; + } + nostromoTestCaseC.investInProject(user, 1, facehuggerMaxInvestAmount); + break; + case 2: + if (ct < 4000) + { + totalInvestedAmount_2 += chestburstMaxInvestAmount; + } + nostromoTestCaseC.investInProject(user, 1, chestburstMaxInvestAmount); + break; + case 3: + if (ct < 4000) + { + totalInvestedAmount_2 += dogMaxInvestAmount; + } + nostromoTestCaseC.investInProject(user, 1, dogMaxInvestAmount); + break; + case 4: + totalInvestedAmount_2 += xenomorphMaxInvestAmount; + nostromoTestCaseC.investInProject(user, 1, xenomorphMaxInvestAmount); + break; + case 5: + totalInvestedAmount_2 += warriorMaxInvestAmount; + nostromoTestCaseC.investInProject(user, 1, warriorMaxInvestAmount); + break; + + default: + break; + } + + duplicatedUser[user] = 1; + } + + nostromoTestCaseC.getState()->totalRaisedFundChecker(1, totalInvestedAmount_2, assetName); + EXPECT_EQ(originalSCBalance + totalInvestedAmount_2 - NOSTROMO_QX_TOKEN_ISSUANCE_FEE, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); + + // getStats function Checker + nostromoTestCaseC.getState()->getStatsChecker(epochRevenu_t, totalPoolWeight, numberOfCreatedProject_t, numberOfFundraising_t, countOfRegister); + + utcTime.Year = 2025; + utcTime.Month = 7; + utcTime.Day = 24; + utcTime.Hour = 0; + updateQpiTime(); + + uint64 originalCreatorBalance = getBalance(registers[0]); + nostromoTestCaseC.endEpoch(); + EXPECT_EQ(getBalance(registers[0]) - originalCreatorBalance, totalInvestedAmount - div(totalInvestedAmount * 5, 100ULL) + totalInvestedAmount_2 - div(totalInvestedAmount_2 * 5, 100ULL)); + nostromoTestCaseC.getState()->endEpochSucceedFundraisingChecker(registers[0], 1, totalInvestedAmount_2, originalCreatorBalance, assetName); + + // EndEpochFailedFundraising Checker + utcTime.Year = 2025; + utcTime.Month = 6; + utcTime.Day = 20; + utcTime.Hour = 0; + updateQpiTime(); + + increaseEnergy(registers[0], NOSTROMO_CREATE_PROJECT_FEE); + assetName = assetNameFromString("BBBB"); + nostromoTestCaseC.createProject(registers[0], assetName, 21000000, 25, 6, 22, 0, 25, 6, 25, 0); + numberOfCreatedProject_t++; + epochRevenu_t += 100000000; + + // getProjectIndexListByCreator function checker + NOST::getProjectIndexListByCreator_output getProjectIndexListByCreator_output = nostromoTestCaseC.getProjectIndexListByCreator(registers[0]); + for (uint32 i = 0; i < 128; i++) + { + if (i < 3) + { + EXPECT_EQ(getProjectIndexListByCreator_output.indexListForProjects.get(i), i); + } + else { + EXPECT_EQ(getProjectIndexListByCreator_output.indexListForProjects.get(i), 262144); + } + } + + utcTime.Year = 2025; + utcTime.Month = 6; + utcTime.Day = 23; + utcTime.Hour = 0; + updateQpiTime(); + + Ynumber = 0; Nnumber = 0; + duplicatedUser.clear(); + + for (const auto& user : registers) + { + if (duplicatedUser[user]) + { + continue; + } + + bit decision = (bit)random(0, 3); + if (decision) + { + Ynumber++; + } + else + { + Nnumber++; + } + + nostromoTestCaseC.voteInProject(user, 2, decision); + duplicatedUser[user] = 1; + } + nostromoTestCaseC.getState()->voteInProjectChecker(2, Ynumber, Nnumber); + + utcTime.Year = 2025; + utcTime.Month = 6; + utcTime.Day = 26; + utcTime.Hour = 0; + updateQpiTime(); + increaseEnergy(registers[0], NOSTROMO_QX_TOKEN_ISSUANCE_FEE); + + nostromoTestCaseC.createFundraising(registers[0], 100000, 2000000, 150000000000, 2, + 25, 6, 27, 0, + 25, 7, 5, 0, + 25, 7, 8, 0, + 25, 7, 10, 0, + 25, 7, 20, 0, + 25, 7, 23, 0, + 25, 7, 25, 0, + 25, 7, 27, 0, + 26, 7, 27, 0, + 20, 10, 12); + numberOfFundraising_t++; + + nostromoTestCaseC.getState()->countOfFundraisingChecker(3); + nostromoTestCaseC.getState()->createFundraisingChecker(registers[0], 100000, 2000000, 150000000000, 2, + 25, 6, 27, 0, + 25, 7, 5, 0, + 25, 7, 8, 0, + 25, 7, 10, 0, + 25, 7, 20, 0, + 25, 7, 23, 0, + 25, 7, 25, 0, + 25, 7, 27, 0, + 26, 7, 27, 0, + 20, 10, 12, 2); + + utcTime.Year = 2025; + utcTime.Month = 6; + utcTime.Day = 27; + utcTime.Hour = 1; + updateQpiTime(); + + uint64 totalInvestedAmount_3 = 0; + duplicatedUser.clear(); + ct = 0; + originalSCBalance = getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0)); + for (const auto& user : registers) + { + if (duplicatedUser[user]) + { + ct++; + continue; + } + ct++; + increaseEnergy(user, 180000000000); + uint8 tierLevel = nostromoTestCaseC.getState()->getTierLevel(user); + + if (ct >= 4000) + { + + // Phase 2 Investment + utcTime.Year = 2025; + utcTime.Month = 7; + utcTime.Day = 9; + utcTime.Hour = 0; + updateQpiTime(); + } + + bit sg = 0; + switch (tierLevel) + { + case 1: + if (ct < 4000) + { + if (totalInvestedAmount_3 + facehuggerMaxInvestAmount > 120000000000) + { + sg = 1; + break; + } + totalInvestedAmount_3 += facehuggerMaxInvestAmount; + } + nostromoTestCaseC.investInProject(user, 2, facehuggerMaxInvestAmount); + break; + case 2: + if (ct < 4000) + { + if (totalInvestedAmount_3 + chestburstMaxInvestAmount > 120000000000) + { + sg = 1; + break; + } + totalInvestedAmount_3 += chestburstMaxInvestAmount; + } + nostromoTestCaseC.investInProject(user, 2, chestburstMaxInvestAmount); + break; + case 3: + if (ct < 4000) + { + if (totalInvestedAmount_3 + dogMaxInvestAmount > 120000000000) + { + sg = 1; + break; + } + totalInvestedAmount_3 += dogMaxInvestAmount; + } + nostromoTestCaseC.investInProject(user, 2, dogMaxInvestAmount); + break; + case 4: + if (totalInvestedAmount_3 + xenomorphMaxInvestAmount > 120000000000) + { + sg = 1; + break; + } + totalInvestedAmount_3 += xenomorphMaxInvestAmount; + nostromoTestCaseC.investInProject(user, 2, xenomorphMaxInvestAmount); + break; + case 5: + if (totalInvestedAmount_3 + warriorMaxInvestAmount > 120000000000) + { + sg = 1; + break; + } + totalInvestedAmount_3 += warriorMaxInvestAmount; + nostromoTestCaseC.investInProject(user, 2, warriorMaxInvestAmount); + break; + + default: + break; + } + + if (sg) + { + break; + } + + duplicatedUser[user] = 1; + } + + nostromoTestCaseC.getState()->totalRaisedFundChecker(2, totalInvestedAmount_3, assetName); + + utcTime.Year = 2025; + utcTime.Month = 7; + utcTime.Day = 24; + utcTime.Hour = 0; + updateQpiTime(); + + originalCreatorBalance = getBalance(registers[0]); + EXPECT_EQ(originalSCBalance + totalInvestedAmount_3, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); + + uint64 epochRevenue = nostromoTestCaseC.getState()->getEpochRevenue(); + uint64 teamFee = div(epochRevenue, 10ULL); + epochRevenue -= teamFee; + nostromoTestCaseC.endEpoch(); + + EXPECT_EQ(originalSCBalance + totalInvestedAmount_3 - teamFee - (div(epochRevenue, 676ULL) * 676), getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); + nostromoTestCaseC.getState()->endEpochFailedFundraisingChecker(2); + nostromoTestCaseC.getState()->endEpochVoteStatusClearChecker(); } From 406f78078cf3034a5596ebe3ae34087f196f7eb1 Mon Sep 17 00:00:00 2001 From: fnordspace Date: Wed, 2 Sep 2026 13:11:45 +0200 Subject: [PATCH 21/21] Update version number --- src/public_settings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/public_settings.h b/src/public_settings.h index 66d53cd8..0a0a92b9 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -74,7 +74,7 @@ static_assert(AUTO_FORCE_NEXT_TICK_THRESHOLD* TARGET_TICK_DURATION >= PEER_REFRE #define VERSION_A 1 #define VERSION_B 303 -#define VERSION_C 0 +#define VERSION_C 1 // Epoch and initial tick for node startup #define EPOCH 229