diff --git a/.github/workflows/contract-verify.yml b/.github/workflows/contract-verify.yml index ae38191e0..f40a97640 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: Franziska-Mueller/qubic-contract-verify@main + uses: qubic/contract-verify@970ce102d56df53b68f1b8fa65b2dd445d5c9d81 # main with: filepaths: '${{ steps.filepaths.outputs.contract-filepaths }},${{ steps.filepaths.outputs.oi-filepaths }}' diff --git a/data/bpp9000.task b/data/bpp9000.task index 8ffa5e17a..38ce23517 100644 Binary files a/data/bpp9000.task and b/data/bpp9000.task differ diff --git a/doc/ant_colony_mining.md b/doc/ant_colony_mining.md index 881d65018..4a996840f 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/doc/contracts.md b/doc/contracts.md index c7f054802..1ba55265d 100644 --- a/doc/contracts.md +++ b/doc/contracts.md @@ -105,7 +105,7 @@ In order to develop a contract, follow these steps: ## Review and tests Each contract must be validated with the following steps: -1. The contract is verified with the [Qubic Contract Verification Tool](https://github.com/Franziska-Mueller/qubic-contract-verify), ensuring that it complies with the formal requirements mentioned above, such as no use of [forbidden C++ features](#restrictions-of-c-language-features). +1. The contract is verified with the [Qubic Contract Verification Tool](https://github.com/qubic/contract-verify), ensuring that it complies with the formal requirements mentioned above, such as no use of [forbidden C++ features](#restrictions-of-c-language-features). In the `qubic/core` repository, the tool is run automatically as GitHub workflow for PRs to the `develop` and `main` branches (as well as for commits in these branches). However, since workflow runs on PRs require maintainer approval, we highly recommend to either build the tool from source or use the GitHub action provided in the tool's repository to analyze your contract header file before opening your PR. 2. The features of the contract have to be extensively tested with automated tests implemented within the Qubic Core's GoogleTest framework. diff --git a/doc/contracts_oracles.md b/doc/contracts_oracles.md index e16ed1bed..76dfe8080 100644 --- a/doc/contracts_oracles.md +++ b/doc/contracts_oracles.md @@ -194,7 +194,7 @@ static sint64 getSubscriptionFee(const OracleQuery& query, uint32 notifyPeriodIn Additionally, the interface struct may contain other structs or convenience features for contracts using the oracle interface. All code in the interface header file must respect the same [C++ language feature restrictions as contracts](#restrictions-of-c-language-features) (except for a few differences listed below). -These are checked with the [Qubic Contract Verification Tool](https://github.com/Franziska-Mueller/qubic-contract-verify). +These are checked with the [Qubic Contract Verification Tool](https://github.com/qubic/contract-verify). The C++ language feature restrictions of oracle interfaces differ in the following points from those of contracts: diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index cce92d29a..04add7298 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -26,7 +26,6 @@ - @@ -44,7 +43,6 @@ - diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index 32882eaf4..10cc5247b 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -126,9 +126,6 @@ contracts - - contracts - contracts @@ -245,9 +242,6 @@ contracts - - contracts - platform diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 6e292e289..300adc1a4 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 @@ -209,11 +205,7 @@ #define CONTRACT_INDEX QRAFFLE_CONTRACT_INDEX #define CONTRACT_STATE_TYPE QRAFFLE #define CONTRACT_STATE2_TYPE QRAFFLE2 -#ifdef OLD_QRAFFLE -#include "contracts/QRaffle_old.h" -#else #include "contracts/QRaffle.h" -#endif #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE @@ -584,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 } }; +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 1e50441e1..e3e34c840 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/contracts/QIP.h b/src/contracts/QIP.h index bec11e1e3..de4483d29 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/src/contracts/QRaffle_old.h b/src/contracts/QRaffle_old.h deleted file mode 100644 index 453612b3e..000000000 --- a/src/contracts/QRaffle_old.h +++ /dev/null @@ -1,2836 +0,0 @@ -using namespace QPI; - -constexpr uint64 QRAFFLE_REGISTER_AMOUNT = 1000000000ull; -constexpr uint64 QRAFFLE_QXMR_REGISTER_AMOUNT = 250000000ull; -constexpr uint64 QRAFFLE_MAX_QRE_AMOUNT = 1000000000ull; -constexpr uint64 QRAFFLE_ASSET_NAME = 19505638103142993; -constexpr uint64 QRAFFLE_QXMR_ASSET_NAME = 1380800593; // QXMR token asset name -constexpr uint32 QRAFFLE_LOGOUT_FEE = div(QRAFFLE_REGISTER_AMOUNT, 20); -constexpr uint32 QRAFFLE_QXMR_LOGOUT_FEE = div(QRAFFLE_QXMR_REGISTER_AMOUNT, 20); // QXMR logout fee -constexpr uint32 QRAFFLE_TRANSFER_SHARE_FEE = 100; -constexpr uint32 QRAFFLE_BURN_FEE = 5; // percent -constexpr uint32 QRAFFLE_REGISTER_FEE = 5; // percent -constexpr uint32 QRAFFLE_FEE = 1; // percent -constexpr uint32 QRAFFLE_CHARITY_FEE = 1; // percent -constexpr uint32 QRAFFLE_SHAREHOLDER_FEE = 8; // percent -constexpr uint32 QRAFFLE_MAX_EPOCH = 65536; -constexpr uint32 QRAFFLE_MAX_PROPOSAL_EPOCH = 128; -constexpr uint32 QRAFFLE_MAX_MEMBER = 65536; -constexpr uint32 QRAFFLE_DEFAULT_QRAFFLE_AMOUNT = 10000000ull; -constexpr uint32 QRAFFLE_MIN_QRAFFLE_AMOUNT = 1000000ull; -constexpr uint32 QRAFFLE_MAX_QRAFFLE_AMOUNT = 1000000000ull; -// Ended token-raffle ring: 16 384 slots × ~96 B ≈ 1.5 MB. -// At most QRAFFLE_MAX_PROPOSAL_EPOCH (128) raffles/epoch → covers ~128 epochs of history. -constexpr uint32 QRAFFLE_MAX_TOKEN_RAFFLES = 16384; -constexpr uint32 QRAFFLE_TOKEN_RAFFLE_SLOT_SIZE = 512; // 2^9, max members per token raffle -constexpr uint8 QRAFFLE_MAX_PROPOSALS_PER_PROPOSER = 3; // max proposals per user per epoch - -constexpr sint32 QRAFFLE_SUCCESS = 0; -constexpr sint32 QRAFFLE_INSUFFICIENT_FUND = 1; -constexpr sint32 QRAFFLE_ALREADY_REGISTERED = 2; -constexpr sint32 QRAFFLE_UNREGISTERED = 3; -constexpr sint32 QRAFFLE_MAX_PROPOSAL_EPOCH_REACHED = 4; -constexpr sint32 QRAFFLE_INVALID_PROPOSAL = 5; -constexpr sint32 QRAFFLE_FAILED_TO_DEPOSIT = 6; -constexpr sint32 QRAFFLE_ALREADY_VOTED = 7; -constexpr sint32 QRAFFLE_INVALID_TOKEN_RAFFLE = 8; -constexpr sint32 QRAFFLE_INVALID_OFFSET_OR_LIMIT = 9; -constexpr sint32 QRAFFLE_INVALID_EPOCH = 10; -constexpr sint32 QRAFFLE_MAX_MEMBER_REACHED = 11; -constexpr sint32 QRAFFLE_INITIAL_REGISTER_CANNOT_LOGOUT = 12; -constexpr sint32 QRAFFLE_INSUFFICIENT_QXMR = 13; -constexpr sint32 QRAFFLE_INVALID_TOKEN_TYPE = 14; -constexpr sint32 QRAFFLE_USER_NOT_FOUND = 15; -constexpr sint32 QRAFFLE_INVALID_ENTRY_AMOUNT = 16; -constexpr sint32 QRAFFLE_EMPTY_QU_RAFFLE = 17; -constexpr sint32 QRAFFLE_EMPTY_TOKEN_RAFFLE = 18; -constexpr sint32 QRAFFLE_MAX_PROPOSAL_PER_USER_REACHED = 19; - -// Asset Raffle return codes -constexpr sint32 QRAFFLE_INVALID_BUNDLE = 20; -constexpr sint32 QRAFFLE_INVALID_RESERVE_PRICE = 21; -constexpr sint32 QRAFFLE_BUNDLE_ESCROW_FAILED = 22; -constexpr sint32 QRAFFLE_INVALID_ASSET_RAFFLE = 23; -constexpr sint32 QRAFFLE_ASSET_RAFFLE_FULL = 24; -constexpr sint32 QRAFFLE_TICKET_LIMIT_REACHED = 25; -constexpr sint32 QRAFFLE_MAX_ASSET_RAFFLES_REACHED = 26; -constexpr sint32 QRAFFLE_CANCEL_NOT_ALLOWED = 27; - -// Asset Raffle configuration -constexpr uint64 QRAFFLE_ASSET_RAFFLE_PROPOSAL_FEE = 500000ull; // 500K Qu; non-refundable anti-spam -constexpr uint32 QRAFFLE_MAX_ASSET_RAFFLES_PER_EPOCH = 64; // concurrent active raffles -constexpr uint32 QRAFFLE_MAX_ASSETS_PER_BUNDLE = 4; // items per bundle -constexpr uint32 QRAFFLE_MAX_ASSET_TICKET_BUYERS = 1024; // distinct buyers per raffle -constexpr uint32 QRAFFLE_MAX_TICKETS_PER_BUYER = 100; // per-buyer cap (anti-griefing) -constexpr uint32 QRAFFLE_MAX_ASSET_RAFFLES_PER_CREATOR = 2; // per creator per epoch -constexpr uint32 QRAFFLE_MAX_ENDED_ASSET_RAFFLES = 8192; // history ring buffer -constexpr uint64 QRAFFLE_MIN_ASSET_TICKET_AMOUNT = 1000000ull; // 1M Qu -constexpr uint64 QRAFFLE_MAX_ASSET_TICKET_AMOUNT = 1000000000000ull;// 1T Qu -// Flat array strides: raffle i occupies [i*stride .. i*stride+count) -constexpr uint32 QRAFFLE_ASSET_RAFFLE_BUNDLE_FLAT_SIZE = QRAFFLE_MAX_ASSET_RAFFLES_PER_EPOCH * QRAFFLE_MAX_ASSETS_PER_BUNDLE; // 256 -constexpr uint32 QRAFFLE_ASSET_RAFFLE_BUYERS_FLAT_SIZE = QRAFFLE_MAX_ASSET_RAFFLES_PER_EPOCH * QRAFFLE_MAX_ASSET_TICKET_BUYERS; // 65536 - - -constexpr uint32 QRAFFLE_MAX_SHAREHOLDERS_OLD = 1024; - - -struct QRAFFLE2 -{ -}; - -struct QRAFFLE : public ContractBase -{ -public: - enum LogInfo { - QRAFFLE_success = 0, - QRAFFLE_insufficientQubic = 1, - QRAFFLE_insufficientQXMR = 2, - QRAFFLE_alreadyRegistered = 3, - QRAFFLE_unregistered = 4, - QRAFFLE_maxMemberReached = 5, - QRAFFLE_maxProposalEpochReached = 6, - QRAFFLE_invalidProposal = 7, - QRAFFLE_failedToDeposit = 8, - QRAFFLE_alreadyVoted = 9, - QRAFFLE_invalidTokenRaffle = 10, - QRAFFLE_invalidOffsetOrLimit = 11, - QRAFFLE_invalidEpoch = 12, - QRAFFLE_initialRegisterCannotLogout = 13, - QRAFFLE_invalidTokenType = 14, - QRAFFLE_invalidEntryAmount = 15, - QRAFFLE_maxMemberReachedForQuRaffle = 16, - QRAFFLE_proposalNotFound = 17, - QRAFFLE_proposalAlreadyEnded = 18, - QRAFFLE_notEnoughShares = 19, - QRAFFLE_transferFailed = 20, - QRAFFLE_epochEnded = 21, - QRAFFLE_winnerSelected = 22, - QRAFFLE_revenueDistributed = 23, - QRAFFLE_tokenRaffleCreated = 24, - QRAFFLE_tokenRaffleEnded = 25, - QRAFFLE_proposalSubmitted = 26, - QRAFFLE_proposalVoted = 27, - QRAFFLE_quRaffleDeposited = 28, - QRAFFLE_tokenRaffleDeposited = 29, - QRAFFLE_shareManagementRightsTransferred = 30, - QRAFFLE_emptyQuRaffle = 31, - QRAFFLE_emptyTokenRaffle = 32, - QRAFFLE_maxProposalPerUserReached = 33, - // Asset Raffle log types - QRAFFLE_assetRaffleCreated = 34, - QRAFFLE_assetRaffleTicketBought = 35, - QRAFFLE_assetRaffleSucceeded = 36, - QRAFFLE_assetRaffleRefunded = 37, - QRAFFLE_assetRaffleBundleEscrowFailed = 38, - QRAFFLE_assetRaffleBundleDeliveryFailed = 39, - QRAFFLE_assetRaffleCancelled = 40, - QRAFFLE_invalidBundle = 41, - QRAFFLE_invalidReservePrice = 42, - QRAFFLE_assetRaffleFull = 43, - QRAFFLE_ticketLimitReached = 44, - QRAFFLE_maxAssetRafflesReached = 45, - QRAFFLE_cancelNotAllowed = 46 - }; - - struct Logger - { - uint32 _contractIndex; - uint32 _type; // Assign a random unique (per contract) number to distinguish messages of different types - sint8 _terminator; // Only data before "_terminator" are logged - }; - - // Enhanced logger for END_EPOCH with detailed information - struct EndEpochLogger - { - uint32 _contractIndex; - uint32 _type; - uint32 _epoch; // Current epoch number - uint32 _memberCount; // Number of QuRaffle members - uint64 _totalAmount; // Total amount being processed - uint64 _winnerAmount; // Amount won by winner - uint32 _winnerIndex; // Index of the winner - sint8 _terminator; - }; - - // Enhanced logger for revenue distribution - struct RevenueLogger - { - uint32 _contractIndex; - uint32 _type; - uint64 _burnAmount; // Amount burned - uint64 _charityAmount; // Amount sent to charity - uint64 _shareholderAmount; // Amount distributed to shareholders - uint64 _registerAmount; // Amount distributed to registers - uint64 _feeAmount; // Amount sent to fee address - uint64 _winnerAmount; // Amount sent to winner - sint8 _terminator; - }; - - // Enhanced logger for token raffle processing - struct TokenRaffleLogger - { - uint32 _contractIndex; - uint32 _type; - uint32 _raffleIndex; // Index of the token raffle - uint64 _assetName; // Asset name of the token - uint32 _memberCount; // Number of members in this raffle - uint64 _entryAmount; // Entry amount for this raffle - uint32 _winnerIndex; // Winner index for this raffle - uint64 _winnerAmount; // Amount won in this raffle - sint8 _terminator; - }; - - // Enhanced logger for proposal processing - struct ProposalLogger - { - uint32 _contractIndex; - uint32 _type; - uint32 _proposalIndex; // Index of the proposal - id _proposer; // Proposer of the proposal - uint32 _yesVotes; // Number of yes votes - uint32 _noVotes; // Number of no votes - uint64 _assetName; // Asset name if approved - uint64 _entryAmount; // Entry amount if approved - sint8 _terminator; - }; - - struct EmptyTokenRaffleLogger - { - uint32 _contractIndex; - uint32 _type; - uint32 _tokenRaffleIndex; // Index of the token raffle per epoch - sint8 _terminator; - }; - - struct AssetRaffleCreatedLogger - { - uint32 _contractIndex; - uint32 _type; - uint32 _raffleIndex; - id _creator; - uint64 _reservePriceQu; - uint64 _entryTicketQu; - uint32 _bundleSize; - sint8 _terminator; - }; - - struct AssetRaffleTicketLogger - { - uint32 _contractIndex; - uint32 _type; - uint32 _raffleIndex; - id _buyer; - uint32 _tickets; - uint64 _cost; - sint8 _terminator; - }; - - struct AssetRaffleEndedLogger - { - uint32 _contractIndex; - uint32 _type; - uint32 _raffleIndex; - id _creator; - id _winner; - uint64 _grossPoolQu; - uint64 _creatorPaidQu; - uint8 _reserveMet; - sint8 _terminator; - }; - - // One item in an asset raffle bundle (token or SC share + quantity). - struct AssetRaffleItem - { - Asset asset; - sint64 numberOfShares; - }; - - // Active asset raffle state (live during the epoch it was created). - struct AssetRaffleInfo - { - id creator; - uint64 reservePriceQu; // net Qu creator wants AFTER 20% fee - uint64 entryTicketQu; // Qu per ticket - uint64 totalTicketsPaidQu; // gross Qu pool so far - uint32 numberOfBuyers; - uint32 totalTickets; - uint32 bundleSize; - uint32 epoch; - }; - - // Historical record written at END_EPOCH for each settled asset raffle. - // Field order keeps the largest types first so trailing padding is minimal and - // deterministic across compilers (no explicit pad needed → no plain C arrays). - struct EndedAssetRaffleInfo - { - id creator; - id epochWinner; // NULL_ID if reserve was missed - uint64 reservePriceQu; - uint64 entryTicketQu; - uint64 grossPoolQu; - uint64 creatorPaidQu; // 0 if reserve missed - uint32 totalTickets; - uint32 numberOfBuyers; - uint32 bundleSize; - uint32 epoch; - uint32 reserveMet; // 1 = reserve met and winner paid; 0 = refunded (uint32 keeps natural alignment) - }; - - struct ProposalInfo { - Asset token; - id proposer; - uint64 entryAmount; - uint32 nYes; - uint32 nNo; - }; - - struct QuRaffleInfo - { - id epochWinner; - uint64 receivedAmount; - uint64 entryAmount; - uint32 numberOfMembers; - uint32 winnerIndex; - }; - - struct TokenRaffleInfo - { - id epochWinner; - Asset token; - uint64 entryAmount; - uint32 numberOfMembers; - uint32 winnerIndex; - uint32 epoch; - }; - - struct ActiveTokenRaffleInfo { - Asset token; - uint64 entryAmount; - }; - - struct OldStateData - { - HashMap registers; - Array proposals; - - HashMap , QRAFFLE_MAX_MEMBER> voteParticipation; - HashMap , QRAFFLE_MAX_MEMBER> voteValues; - Array numberOfVotedInProposal; - Array quRaffleMembers; - HashSet quRaffleMemberSet; - - Array activeTokenRaffle; - HashMap , QRAFFLE_MAX_MEMBER> tokenRaffleParticipation; - Array tokenRaffleMemberSlots; - Array numberOfTokenRaffleMembers; - - Array QuRaffles; - Array tokenRaffle; - HashMap quRaffleEntryAmount; - HashSet shareholdersList; - - id initialRegister1, initialRegister2, initialRegister3, initialRegister4, initialRegister5; - id charityAddress, feeAddress, QXMRIssuer; - uint64 epochRevenue, epochQXMRRevenue, qREAmount, totalBurnAmount, totalCharityAmount, totalShareholderAmount, totalRegisterAmount, totalFeeAmount, totalWinnerAmount, largestWinnerAmount; - uint32 numberOfRegisters, numberOfQuRaffleMembers, numberOfEntryAmountSubmitted, numberOfProposals, numberOfActiveTokenRaffle, numberOfEndedTokenRaffle; - Array daoMemberCount; - HashMap proposalsPerProposer; - }; - - struct StateData - { - HashMap registers; - Array proposals; - - // Per-user vote tracking with dual BitArray (qRWA pattern). - // O(1) lookup via id hash, 1 bit per proposal. ~4 MB each, ~8 MB total. - HashMap , QRAFFLE_MAX_MEMBER> voteParticipation; // bit=1 if user has voted - HashMap , QRAFFLE_MAX_MEMBER> voteValues; // bit=1 for yes, bit=0 for no - Array numberOfVotedInProposal; - Array quRaffleMembers; - // O(1) duplicate guard for quRaffle entries; mirrors quRaffleMembers for membership tests. - HashSet quRaffleMemberSet; - - Array activeTokenRaffle; - // Per-user O(1) duplicate check for token raffle deposits. ~4 MB. - HashMap , QRAFFLE_MAX_MEMBER> tokenRaffleParticipation; - // Flat indexed member storage: raffle i occupies slots [i*SLOT_SIZE .. i*SLOT_SIZE+count). ~2 MB. - Array tokenRaffleMemberSlots; - Array numberOfTokenRaffleMembers; - - Array QuRaffles; - Array tokenRaffle; - HashMap quRaffleEntryAmount; - - id initialRegister1, initialRegister2, initialRegister3, initialRegister4, initialRegister5; - id charityAddress, feeAddress, QXMRIssuer; - uint64 epochRevenue, epochQXMRRevenue, qREAmount, totalBurnAmount, totalCharityAmount, totalShareholderAmount, totalRegisterAmount, totalFeeAmount, totalWinnerAmount, largestWinnerAmount; - uint32 numberOfRegisters, numberOfQuRaffleMembers, numberOfEntryAmountSubmitted, numberOfProposals, numberOfActiveTokenRaffle, numberOfEndedTokenRaffle; - Array daoMemberCount; // Number of DAO members (registers) at each epoch - HashMap proposalsPerProposer; - - // ── Asset Raffle state ──────────────────────────────────────────────────────── - Array activeAssetRaffles; - uint32 numberOfActiveAssetRaffles; - - // Bundle items: raffle i occupies [i*4 .. i*4+bundleSize). - Array activeAssetRaffleItems; - - // Buyer lists: raffle i occupies [i*1024 .. i*1024+numberOfBuyers). - Array activeAssetRaffleBuyers; - Array activeAssetRaffleBuyerTickets; - - // O(1) has-bought check: bit[i]=1 means this user has tickets in raffle i. - HashMap, QRAFFLE_MAX_MEMBER> assetRaffleParticipation; - - // O(1) slot lookup: entry[i] = buyer's 0-based position in raffle i's buyer region. - // Sentinel 0xFFFF = not present. ~8 MB (64 raffles × 2 B × 65536 buyers). - // Reset each epoch alongside the buyer arrays. - HashMap, QRAFFLE_MAX_MEMBER> assetRaffleBuyerSlotIndex; - - // Per-creator raffle count; reset each epoch to enforce QRAFFLE_MAX_ASSET_RAFFLES_PER_CREATOR. - HashMap assetRafflesPerCreator; - - // Settled raffle history ring buffer. - Array endedAssetRaffles; - uint32 numberOfEndedAssetRaffles; - - // Accumulated proposal fees destined for DAO registers (50% of each 500K fee); - // distributed in one O(R) pass at END_EPOCH alongside the register share bucket. - uint64 epochAssetRaffleDaoBucket; - - // Aggregate analytics (monotonically increasing). - uint64 totalAssetRaffleProposalFees; - uint64 totalAssetRaffleCreatorPaid; - uint64 totalAssetRaffleRefunded; - uint32 totalAssetRafflesCreated; - uint32 totalAssetRafflesSucceeded; - uint32 totalAssetRafflesFailed; - }; - - struct registerInSystem_input - { - bit useQXMR; // 0 = use qubic, 1 = use QXMR tokens - }; - - struct registerInSystem_output - { - sint32 returnCode; - }; - - struct logoutInSystem_input - { - }; - - struct logoutInSystem_output - { - sint32 returnCode; - }; - - struct submitEntryAmount_input - { - uint64 amount; - }; - - struct submitEntryAmount_output - { - sint32 returnCode; - }; - - struct submitProposal_input - { - id tokenIssuer; - uint64 tokenName; - uint64 entryAmount; - }; - - struct submitProposal_output - { - sint32 returnCode; - }; - - struct voteInProposal_input - { - uint32 indexOfProposal; - bit yes; - }; - - struct voteInProposal_output - { - sint32 returnCode; - }; - - struct depositInQuRaffle_input - { - - }; - - struct depositInQuRaffle_output - { - sint32 returnCode; - }; - - struct depositInTokenRaffle_input - { - uint32 indexOfTokenRaffle; - }; - - struct depositInTokenRaffle_output - { - sint32 returnCode; - }; - - struct TransferShareManagementRights_input - { - id tokenIssuer; - uint64 tokenName; - sint64 numberOfShares; - uint32 newManagingContractIndex; - }; - - struct TransferShareManagementRights_output - { - sint64 transferredNumberOfShares; - }; - - struct getRegisters_input - { - uint32 offset; - uint32 limit; - }; - - struct getRegisters_output - { - id register1, register2, register3, register4, register5, register6, register7, register8, register9, register10, register11, register12, register13, register14, register15, register16, register17, register18, register19, register20; - sint32 returnCode; - }; - - struct getAnalytics_input - { - }; - - struct getAnalytics_output - { - uint64 currentQuRaffleAmount; - uint64 totalBurnAmount; - uint64 totalCharityAmount; - uint64 totalShareholderAmount; - uint64 totalRegisterAmount; - uint64 totalFeeAmount; - uint64 totalWinnerAmount; - uint64 largestWinnerAmount; - uint32 numberOfRegisters; - uint32 numberOfProposals; - uint32 numberOfQuRaffleMembers; - uint32 numberOfActiveTokenRaffle; - uint32 numberOfEndedTokenRaffle; - uint32 numberOfEntryAmountSubmitted; - sint32 returnCode; - }; - - struct getActiveProposal_input - { - uint32 indexOfProposal; - }; - - struct getActiveProposal_output - { - id tokenIssuer; - id proposer; - uint64 tokenName; - uint64 entryAmount; - uint32 nYes; - uint32 nNo; - sint32 returnCode; - }; - - struct getEndedTokenRaffle_input - { - uint32 indexOfRaffle; - }; - - struct getEndedTokenRaffle_output - { - id epochWinner; - id tokenIssuer; - uint64 tokenName; - uint64 entryAmount; - uint32 numberOfMembers; - uint32 winnerIndex; - uint32 epoch; - sint32 returnCode; - }; - - struct getEpochRaffleIndexes_input - { - uint32 epoch; - }; - - struct getEpochRaffleIndexes_output - { - uint32 StartIndex; - uint32 EndIndex; - sint32 returnCode; - }; - - struct getEndedQuRaffle_input - { - uint32 epoch; - }; - - struct getEndedQuRaffle_output - { - id epochWinner; - uint64 receivedAmount; - uint64 entryAmount; - uint32 numberOfMembers; - uint32 winnerIndex; - uint32 numberOfDaoMembers; - sint32 returnCode; - }; - - struct getActiveTokenRaffle_input - { - uint32 indexOfTokenRaffle; - }; - - struct getActiveTokenRaffle_output - { - id tokenIssuer; - uint64 tokenName; - uint64 entryAmount; - uint32 numberOfMembers; - sint32 returnCode; - }; - - struct getQuRaffleEntryAmountPerUser_input - { - id user; - }; - - struct getQuRaffleEntryAmountPerUser_output - { - uint64 entryAmount; - sint32 returnCode; - }; - - struct getQuRaffleEntryAverageAmount_input - { - }; - - struct getQuRaffleEntryAverageAmount_output - { - uint64 entryAverageAmount; - sint32 returnCode; - }; - - // ── Asset Raffle I/O structs ─────────────────────────────────────────────── - - struct createAssetRaffle_input - { - // Bundle: fixed-capacity QPI Array; only [0..bundleSize) are used. - Array bundleItems; - uint32 bundleSize; - uint64 reservePriceQu; // min Qu creator wants AFTER 20% service fee - uint64 entryTicketQu; // Qu per ticket - }; - - struct createAssetRaffle_output - { - uint32 raffleIndex; - sint32 returnCode; - }; - - struct buyAssetRaffleTicket_input - { - uint32 indexOfAssetRaffle; - uint32 numberOfTickets; - }; - - struct buyAssetRaffleTicket_output - { - uint32 ticketsBought; - sint32 returnCode; - }; - - struct cancelAssetRaffle_input - { - uint32 indexOfAssetRaffle; - }; - - struct cancelAssetRaffle_output - { - sint32 returnCode; - }; - - struct getActiveAssetRaffle_input - { - uint32 indexOfAssetRaffle; - }; - - struct getActiveAssetRaffle_output - { - id creator; - uint64 reservePriceQu; - uint64 entryTicketQu; - uint64 totalTicketsPaidQu; - uint32 numberOfBuyers; - uint32 totalTickets; - uint32 bundleSize; - uint32 epoch; - sint32 returnCode; - }; - - struct getActiveAssetRaffleBundleItem_input - { - uint32 indexOfAssetRaffle; - uint32 itemIndex; - }; - - struct getActiveAssetRaffleBundleItem_output - { - id assetIssuer; - uint64 assetName; - sint64 numberOfShares; - sint32 returnCode; - }; - - struct getActiveAssetRaffleBuyer_input - { - uint32 indexOfAssetRaffle; - uint32 buyerIndex; - }; - - struct getActiveAssetRaffleBuyer_output - { - id buyer; - uint32 ticketCount; - sint32 returnCode; - }; - - struct getEndedAssetRaffle_input - { - uint32 indexOfRaffle; - }; - - struct getEndedAssetRaffle_output - { - id creator; - id epochWinner; - uint64 reservePriceQu; - uint64 entryTicketQu; - uint64 grossPoolQu; - uint64 creatorPaidQu; - uint32 totalTickets; - uint32 numberOfBuyers; - uint32 bundleSize; - uint32 epoch; - uint8 reserveMet; - sint32 returnCode; - }; - - struct getAssetRaffleAnalytics_input - { - }; - - struct getAssetRaffleAnalytics_output - { - uint64 totalAssetRaffleProposalFees; - uint64 totalAssetRaffleCreatorPaid; - uint64 totalAssetRaffleRefunded; - uint32 numberOfActiveAssetRaffles; - uint32 numberOfEndedAssetRaffles; - uint32 totalAssetRafflesCreated; - uint32 totalAssetRafflesSucceeded; - uint32 totalAssetRafflesFailed; - sint32 returnCode; - }; - -protected: - - - struct registerInSystem_locals - { - Logger log; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(registerInSystem) - { - if (state.get().registers.contains(qpi.invocator())) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.returnCode = QRAFFLE_ALREADY_REGISTERED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_alreadyRegistered, 0 }; - LOG_INFO(locals.log); - return ; - } - if (state.get().numberOfRegisters >= QRAFFLE_MAX_MEMBER) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.returnCode = QRAFFLE_MAX_MEMBER_REACHED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_maxMemberReached, 0 }; - LOG_INFO(locals.log); - return ; - } - - if (input.useQXMR) - { - // refund the invocation reward if the user uses QXMR for registration - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - // Use QXMR tokens for registration - if (qpi.numberOfPossessedShares(QRAFFLE_QXMR_ASSET_NAME, state.get().QXMRIssuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < QRAFFLE_QXMR_REGISTER_AMOUNT) - { - output.returnCode = QRAFFLE_INSUFFICIENT_QXMR; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQXMR, 0 }; - LOG_INFO(locals.log); - return ; - } - - // Transfer QXMR tokens to the contract - if (qpi.transferShareOwnershipAndPossession(QRAFFLE_QXMR_ASSET_NAME, state.get().QXMRIssuer, qpi.invocator(), qpi.invocator(), QRAFFLE_QXMR_REGISTER_AMOUNT, SELF) < 0) - { - output.returnCode = QRAFFLE_INSUFFICIENT_QXMR; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQXMR, 0 }; - LOG_INFO(locals.log); - return ; - } - state.mut().registers.set(qpi.invocator(), 2); - } - else - { - // Use qubic for registration - if (qpi.invocationReward() < QRAFFLE_REGISTER_AMOUNT) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.returnCode = QRAFFLE_INSUFFICIENT_FUND; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQubic, 0 }; - LOG_INFO(locals.log); - return ; - } - qpi.transfer(qpi.invocator(), qpi.invocationReward() - QRAFFLE_REGISTER_AMOUNT); - state.mut().registers.set(qpi.invocator(), 1); - } - - state.mut().numberOfRegisters++; - output.returnCode = QRAFFLE_SUCCESS; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_success, 0 }; - LOG_INFO(locals.log); - } - - struct logoutInSystem_locals - { - sint64 refundAmount; - uint8 tokenType; - Logger log; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(logoutInSystem) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - if (qpi.invocator() == state.get().initialRegister1 || qpi.invocator() == state.get().initialRegister2 || qpi.invocator() == state.get().initialRegister3 || qpi.invocator() == state.get().initialRegister4 || qpi.invocator() == state.get().initialRegister5) - { - output.returnCode = QRAFFLE_INITIAL_REGISTER_CANNOT_LOGOUT; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_initialRegisterCannotLogout, 0 }; - LOG_INFO(locals.log); - return ; - } - if (state.get().registers.contains(qpi.invocator()) == 0) - { - output.returnCode = QRAFFLE_UNREGISTERED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_unregistered, 0 }; - LOG_INFO(locals.log); - return ; - } - - state.get().registers.get(qpi.invocator(), locals.tokenType); - - if (locals.tokenType == 1) - { - // Use qubic for logout - locals.refundAmount = QRAFFLE_REGISTER_AMOUNT - QRAFFLE_LOGOUT_FEE; - qpi.transfer(qpi.invocator(), locals.refundAmount); - state.mut().epochRevenue += QRAFFLE_LOGOUT_FEE; - } - else if (locals.tokenType == 2) - { - // Use QXMR tokens for logout - locals.refundAmount = QRAFFLE_QXMR_REGISTER_AMOUNT - QRAFFLE_QXMR_LOGOUT_FEE; - - // Check if contract has enough QXMR tokens - if (qpi.numberOfPossessedShares(QRAFFLE_QXMR_ASSET_NAME, state.get().QXMRIssuer, SELF, SELF, SELF_INDEX, SELF_INDEX) < locals.refundAmount) - { - output.returnCode = QRAFFLE_INSUFFICIENT_QXMR; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQXMR, 0 }; - LOG_INFO(locals.log); - return ; - } - - // Transfer QXMR tokens back to user - if (qpi.transferShareOwnershipAndPossession(QRAFFLE_QXMR_ASSET_NAME, state.get().QXMRIssuer, SELF, SELF, locals.refundAmount, qpi.invocator()) < 0) - { - output.returnCode = QRAFFLE_INSUFFICIENT_QXMR; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQXMR, 0 }; - LOG_INFO(locals.log); - return ; - } - - state.mut().epochQXMRRevenue += QRAFFLE_QXMR_LOGOUT_FEE; - } - - state.mut().registers.removeByKey(qpi.invocator()); - state.mut().numberOfRegisters--; - output.returnCode = QRAFFLE_SUCCESS; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_success, 0 }; - LOG_INFO(locals.log); - } - - struct submitEntryAmount_locals - { - Logger log; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(submitEntryAmount) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - if (input.amount < QRAFFLE_MIN_QRAFFLE_AMOUNT || input.amount > QRAFFLE_MAX_QRAFFLE_AMOUNT) - { - output.returnCode = QRAFFLE_INVALID_ENTRY_AMOUNT; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidEntryAmount, 0 }; - LOG_INFO(locals.log); - return ; - } - if (state.get().registers.contains(qpi.invocator()) == 0) - { - output.returnCode = QRAFFLE_UNREGISTERED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_unregistered, 0 }; - LOG_INFO(locals.log); - return ; - } - if (state.get().quRaffleEntryAmount.contains(qpi.invocator()) == 0) - { - state.mut().numberOfEntryAmountSubmitted++; - } - state.mut().quRaffleEntryAmount.set(qpi.invocator(), input.amount); - output.returnCode = QRAFFLE_SUCCESS; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_success, 0 }; - LOG_INFO(locals.log); - } - - struct submitProposal_locals - { - ProposalInfo proposal; - uint8 countThisEpoch; - Logger log; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(submitProposal) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - if (state.get().registers.contains(qpi.invocator()) == 0) - { - output.returnCode = QRAFFLE_UNREGISTERED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_unregistered, 0 }; - LOG_INFO(locals.log); - return ; - } - if (state.get().numberOfProposals >= QRAFFLE_MAX_PROPOSAL_EPOCH) - { - output.returnCode = QRAFFLE_MAX_PROPOSAL_EPOCH_REACHED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_maxProposalEpochReached, 0 }; - LOG_INFO(locals.log); - return ; - } - locals.countThisEpoch = 0; - if (state.get().proposalsPerProposer.contains(qpi.invocator())) - { - state.get().proposalsPerProposer.get(qpi.invocator(), locals.countThisEpoch); - } - if (locals.countThisEpoch >= QRAFFLE_MAX_PROPOSALS_PER_PROPOSER) - { - output.returnCode = QRAFFLE_MAX_PROPOSAL_PER_USER_REACHED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_maxProposalPerUserReached, 0 }; - LOG_INFO(locals.log); - return ; - } - if (input.entryAmount <= 0) - { - output.returnCode = QRAFFLE_INVALID_ENTRY_AMOUNT; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidEntryAmount, 0 }; - LOG_INFO(locals.log); - return ; - } - if (!qpi.isAssetIssued(input.tokenIssuer, input.tokenName)) - { - output.returnCode = QRAFFLE_INVALID_TOKEN_TYPE; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidTokenType, 0 }; - LOG_INFO(locals.log); - return ; - } - - locals.proposal.token.issuer = input.tokenIssuer; - locals.proposal.token.assetName = input.tokenName; - locals.proposal.entryAmount = input.entryAmount; - locals.proposal.proposer = qpi.invocator(); - state.mut().proposals.set(state.get().numberOfProposals, locals.proposal); - state.mut().numberOfProposals++; - state.mut().proposalsPerProposer.set(qpi.invocator(), locals.countThisEpoch + 1); - output.returnCode = QRAFFLE_SUCCESS; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_proposalSubmitted, 0 }; - LOG_INFO(locals.log); - } - - struct voteInProposal_locals - { - ProposalInfo proposal; - BitArray participation; - BitArray values; - uint32 votedCount; - Logger log; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(voteInProposal) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - if (state.get().registers.contains(qpi.invocator()) == 0) - { - output.returnCode = QRAFFLE_UNREGISTERED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_unregistered, 0 }; - LOG_INFO(locals.log); - return ; - } - if (input.indexOfProposal >= state.get().numberOfProposals) - { - output.returnCode = QRAFFLE_INVALID_PROPOSAL; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_proposalNotFound, 0 }; - LOG_INFO(locals.log); - return ; - } - locals.proposal = state.get().proposals.get(input.indexOfProposal); - - // O(1) vote lookup via per-user bitfield (id hash, no K12 overhead). - state.get().voteParticipation.get(qpi.invocator(), locals.participation); - if (locals.participation.get(input.indexOfProposal)) - { - // Already voted — check if same direction. - state.get().voteValues.get(qpi.invocator(), locals.values); - if (locals.values.get(input.indexOfProposal) == input.yes) - { - output.returnCode = QRAFFLE_ALREADY_VOTED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_alreadyVoted, 0 }; - LOG_INFO(locals.log); - return ; - } - // Flip the vote: update counters with underflow guard. - if (input.yes) - { - locals.proposal.nYes++; - if (locals.proposal.nNo > 0) { locals.proposal.nNo--; } - } - else - { - locals.proposal.nNo++; - if (locals.proposal.nYes > 0) { locals.proposal.nYes--; } - } - state.mut().proposals.set(input.indexOfProposal, locals.proposal); - locals.values.set(input.indexOfProposal, input.yes); - state.mut().voteValues.replace(qpi.invocator(), locals.values); - output.returnCode = QRAFFLE_SUCCESS; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_proposalVoted, 0 }; - LOG_INFO(locals.log); - return ; - } - - // New vote: check capacity before inserting. - locals.votedCount = state.get().numberOfVotedInProposal.get(input.indexOfProposal); - if (locals.votedCount >= QRAFFLE_MAX_MEMBER) - { - output.returnCode = QRAFFLE_MAX_MEMBER_REACHED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_maxMemberReached, 0 }; - LOG_INFO(locals.log); - return ; - } - if (input.yes) - { - locals.proposal.nYes++; - } - else - { - locals.proposal.nNo++; - } - state.mut().proposals.set(input.indexOfProposal, locals.proposal); - - // Mark participation and record vote direction. - locals.participation.set(input.indexOfProposal, 1); - state.mut().voteParticipation.set(qpi.invocator(), locals.participation); - state.get().voteValues.get(qpi.invocator(), locals.values); - locals.values.set(input.indexOfProposal, input.yes); - state.mut().voteValues.set(qpi.invocator(), locals.values); - - state.mut().numberOfVotedInProposal.set(input.indexOfProposal, locals.votedCount + 1); - output.returnCode = QRAFFLE_SUCCESS; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_proposalVoted, 0 }; - LOG_INFO(locals.log); - } - - struct depositInQuRaffle_locals - { - Logger log; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(depositInQuRaffle) - { - if (state.get().numberOfQuRaffleMembers >= QRAFFLE_MAX_MEMBER) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.returnCode = QRAFFLE_MAX_MEMBER_REACHED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_maxMemberReachedForQuRaffle, 0 }; - LOG_INFO(locals.log); - return ; - } - if (qpi.invocationReward() < (sint64)state.get().qREAmount) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.returnCode = QRAFFLE_INSUFFICIENT_FUND; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQubic, 0 }; - LOG_INFO(locals.log); - return ; - } - // O(1) duplicate check via HashSet (replaces former O(N) linear scan over quRaffleMembers). - if (state.get().quRaffleMemberSet.contains(qpi.invocator())) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.returnCode = QRAFFLE_ALREADY_REGISTERED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_alreadyRegistered, 0 }; - LOG_INFO(locals.log); - return ; - } - qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.get().qREAmount); - state.mut().quRaffleMembers.set(state.get().numberOfQuRaffleMembers, qpi.invocator()); - state.mut().quRaffleMemberSet.add(qpi.invocator()); - state.mut().numberOfQuRaffleMembers++; - output.returnCode = QRAFFLE_SUCCESS; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_quRaffleDeposited, 0 }; - LOG_INFO(locals.log); - } - - struct depositInTokenRaffle_locals - { - ActiveTokenRaffleInfo raffleInfo; - BitArray participation; - uint32 currentMembers; - Logger log; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(depositInTokenRaffle) - { - if (qpi.invocationReward() < QRAFFLE_TRANSFER_SHARE_FEE) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.returnCode = QRAFFLE_INSUFFICIENT_FUND; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQubic, 0 }; - LOG_INFO(locals.log); - return ; - } - - if (input.indexOfTokenRaffle >= state.get().numberOfActiveTokenRaffle) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.returnCode = QRAFFLE_INVALID_TOKEN_RAFFLE; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidTokenRaffle, 0 }; - LOG_INFO(locals.log); - return ; - } - locals.currentMembers = state.get().numberOfTokenRaffleMembers.get(input.indexOfTokenRaffle); - if (locals.currentMembers >= QRAFFLE_TOKEN_RAFFLE_SLOT_SIZE) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.returnCode = QRAFFLE_MAX_MEMBER_REACHED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_maxMemberReached, 0 }; - LOG_INFO(locals.log); - return ; - } - // O(1) duplicate check via per-user bitfield. - state.get().tokenRaffleParticipation.get(qpi.invocator(), locals.participation); - if (locals.participation.get(input.indexOfTokenRaffle)) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.returnCode = QRAFFLE_ALREADY_REGISTERED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_alreadyRegistered, 0 }; - LOG_INFO(locals.log); - return ; - } - locals.raffleInfo = state.get().activeTokenRaffle.get(input.indexOfTokenRaffle); - if (qpi.transferShareOwnershipAndPossession(locals.raffleInfo.token.assetName, locals.raffleInfo.token.issuer, qpi.invocator(), qpi.invocator(), locals.raffleInfo.entryAmount, SELF) < 0) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.returnCode = QRAFFLE_FAILED_TO_DEPOSIT; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_transferFailed, 0 }; - LOG_INFO(locals.log); - return ; - } - // Keep QRAFFLE_TRANSFER_SHARE_FEE as service revenue; refund any excess. - if (qpi.invocationReward() > QRAFFLE_TRANSFER_SHARE_FEE) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - QRAFFLE_TRANSFER_SHARE_FEE); - } - state.mut().epochRevenue += QRAFFLE_TRANSFER_SHARE_FEE; - - // Store member in flat slot array and mark participation. - state.mut().tokenRaffleMemberSlots.set(input.indexOfTokenRaffle * QRAFFLE_TOKEN_RAFFLE_SLOT_SIZE + locals.currentMembers, qpi.invocator()); - state.mut().numberOfTokenRaffleMembers.set(input.indexOfTokenRaffle, locals.currentMembers + 1); - locals.participation.set(input.indexOfTokenRaffle, 1); - state.mut().tokenRaffleParticipation.set(qpi.invocator(), locals.participation); - output.returnCode = QRAFFLE_SUCCESS; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_tokenRaffleDeposited, 0 }; - LOG_INFO(locals.log); - } - - struct TransferShareManagementRights_locals - { - Asset asset; - sint64 offeredFee; - sint64 paidFee; - Logger log; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(TransferShareManagementRights) - { - // Requires QRAFFLE_TRANSFER_SHARE_FEE minimum. The rest is offered to the destination - // contract as transfer fee; the unused portion is refunded after releaseShares. - if (qpi.invocationReward() < QRAFFLE_TRANSFER_SHARE_FEE) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQubic, 0 }; - LOG_INFO(locals.log); - return ; - } - - if (qpi.numberOfPossessedShares(input.tokenName, input.tokenIssuer,qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.numberOfShares) - { - // Not enough shares — refund in full. - output.transferredNumberOfShares = 0; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_notEnoughShares, 0 }; - LOG_INFO(locals.log); - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - } - else - { - locals.asset.assetName = input.tokenName; - locals.asset.issuer = input.tokenIssuer; - locals.offeredFee = qpi.invocationReward() - QRAFFLE_TRANSFER_SHARE_FEE; - locals.paidFee = qpi.releaseShares(locals.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, - input.newManagingContractIndex, input.newManagingContractIndex, locals.offeredFee); - if (locals.paidFee < 0) - { - // Transfer rejected by the destination — refund everything. - output.transferredNumberOfShares = 0; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_transferFailed, 0 }; - LOG_INFO(locals.log); - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - } - else - { - // Success — keep service fee as revenue, refund unused transfer fee. - output.transferredNumberOfShares = input.numberOfShares; - state.mut().epochRevenue += QRAFFLE_TRANSFER_SHARE_FEE; - if (locals.offeredFee > locals.paidFee) - { - qpi.transfer(qpi.invocator(), locals.offeredFee - locals.paidFee); - } - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_shareManagementRightsTransferred, 0 }; - LOG_INFO(locals.log); - } - } - } - - // ── createAssetRaffle ────────────────────────────────────────────────────── - struct createAssetRaffle_locals - { - AssetRaffleItem item; - AssetRaffleItem dupItem; - AssetRaffleItem rollbackItem; - AssetRaffleInfo info; - AssetRaffleCreatedLogger clog; - Logger log; - uint64 proposalFeeHalf; - uint8 creatorCount; - uint32 slot; - uint32 i; - uint32 j; - sint64 escrowResult; - bit dupFound; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(createAssetRaffle) - { - if (qpi.invocationReward() < (sint64)QRAFFLE_ASSET_RAFFLE_PROPOSAL_FEE) - { - if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.returnCode = QRAFFLE_INSUFFICIENT_FUND; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQubic, 0 }; - LOG_INFO(locals.log); - return; - } - if (!state.get().registers.contains(qpi.invocator())) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - output.returnCode = QRAFFLE_UNREGISTERED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_unregistered, 0 }; - LOG_INFO(locals.log); - return; - } - locals.creatorCount = 0; - if (state.get().assetRafflesPerCreator.contains(qpi.invocator())) - { - state.get().assetRafflesPerCreator.get(qpi.invocator(), locals.creatorCount); - } - if (locals.creatorCount >= QRAFFLE_MAX_ASSET_RAFFLES_PER_CREATOR) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - output.returnCode = QRAFFLE_MAX_ASSET_RAFFLES_REACHED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_maxAssetRafflesReached, 0 }; - LOG_INFO(locals.log); - return; - } - if (state.get().numberOfActiveAssetRaffles >= QRAFFLE_MAX_ASSET_RAFFLES_PER_EPOCH) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - output.returnCode = QRAFFLE_MAX_ASSET_RAFFLES_REACHED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_maxAssetRafflesReached, 0 }; - LOG_INFO(locals.log); - return; - } - if (input.bundleSize == 0 || input.bundleSize > QRAFFLE_MAX_ASSETS_PER_BUNDLE) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - output.returnCode = QRAFFLE_INVALID_BUNDLE; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidBundle, 0 }; - LOG_INFO(locals.log); - return; - } - if (input.entryTicketQu < QRAFFLE_MIN_ASSET_TICKET_AMOUNT || input.entryTicketQu > QRAFFLE_MAX_ASSET_TICKET_AMOUNT) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - output.returnCode = QRAFFLE_INVALID_ENTRY_AMOUNT; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidEntryAmount, 0 }; - LOG_INFO(locals.log); - return; - } - if (input.reservePriceQu == 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - output.returnCode = QRAFFLE_INVALID_RESERVE_PRICE; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidReservePrice, 0 }; - LOG_INFO(locals.log); - return; - } - // Guard against uint64 overflow in the END_EPOCH reserve check (reservePriceQu * 100). - if (input.reservePriceQu > div(0xFFFFFFFFFFFFFFFFull, 100ull)) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - output.returnCode = QRAFFLE_INVALID_RESERVE_PRICE; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidReservePrice, 0 }; - LOG_INFO(locals.log); - return; - } - for (locals.i = 0; locals.i < input.bundleSize; locals.i++) - { - locals.item = input.bundleItems.get(locals.i); - if (!qpi.isAssetIssued(locals.item.asset.issuer, locals.item.asset.assetName)) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - output.returnCode = QRAFFLE_INVALID_BUNDLE; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidBundle, 0 }; - LOG_INFO(locals.log); - return; - } - if (locals.item.numberOfShares <= 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - output.returnCode = QRAFFLE_INVALID_BUNDLE; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidBundle, 0 }; - LOG_INFO(locals.log); - return; - } - // QRAFFLE and QXMR are reserved for dividends/registration; disallow in bundles. - if ((locals.item.asset.assetName == QRAFFLE_ASSET_NAME && locals.item.asset.issuer == NULL_ID) - || (locals.item.asset.assetName == QRAFFLE_QXMR_ASSET_NAME && locals.item.asset.issuer == state.get().QXMRIssuer)) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - output.returnCode = QRAFFLE_INVALID_BUNDLE; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidBundle, 0 }; - LOG_INFO(locals.log); - return; - } - // Duplicate-asset check within the bundle; O(N²) acceptable for N ≤ 4. - locals.dupFound = 0; - for (locals.j = 0; locals.j < locals.i; locals.j++) - { - locals.dupItem = input.bundleItems.get(locals.j); - if (locals.dupItem.asset.assetName == locals.item.asset.assetName - && locals.dupItem.asset.issuer == locals.item.asset.issuer) - { - locals.dupFound = 1; - break; - } - } - if (locals.dupFound) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - output.returnCode = QRAFFLE_INVALID_BUNDLE; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidBundle, 0 }; - LOG_INFO(locals.log); - return; - } - } - - // Atomic escrow: if any item fails, roll back all previously transferred items. - locals.slot = state.get().numberOfActiveAssetRaffles; - for (locals.i = 0; locals.i < input.bundleSize; locals.i++) - { - locals.item = input.bundleItems.get(locals.i); - locals.escrowResult = qpi.transferShareOwnershipAndPossession( - locals.item.asset.assetName, locals.item.asset.issuer, - qpi.invocator(), qpi.invocator(), - locals.item.numberOfShares, SELF); - if (locals.escrowResult < 0) - { - // Rollback: return all previously escrowed items to the invocator. - // These transfers move shares we just received from the same invocator back to - // them, so they should not fail in normal circumstances. If a rollback transfer - // does fail (e.g. spectrum entry evicted), residual shares stay under contract - // management for governance recovery. - for (locals.j = 0; locals.j < locals.i; locals.j++) - { - locals.rollbackItem = input.bundleItems.get(locals.j); - qpi.transferShareOwnershipAndPossession( - locals.rollbackItem.asset.assetName, locals.rollbackItem.asset.issuer, - SELF, SELF, - locals.rollbackItem.numberOfShares, qpi.invocator()); - } - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - output.returnCode = QRAFFLE_BUNDLE_ESCROW_FAILED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_assetRaffleBundleEscrowFailed, 0 }; - LOG_INFO(locals.log); - return; - } - state.mut().activeAssetRaffleItems.set(locals.slot * QRAFFLE_MAX_ASSETS_PER_BUNDLE + locals.i, locals.item); - } - - locals.info.creator = qpi.invocator(); - locals.info.reservePriceQu = input.reservePriceQu; - locals.info.entryTicketQu = input.entryTicketQu; - locals.info.totalTicketsPaidQu = 0; - locals.info.numberOfBuyers = 0; - locals.info.totalTickets = 0; - locals.info.bundleSize = input.bundleSize; - locals.info.epoch = qpi.epoch(); - state.mut().activeAssetRaffles.set(locals.slot, locals.info); - state.mut().numberOfActiveAssetRaffles++; - state.mut().assetRafflesPerCreator.set(qpi.invocator(), locals.creatorCount + 1); - - // 50% of proposal fee → shareholders via epochRevenue; 50% → DAO bucket distributed in END_EPOCH. - locals.proposalFeeHalf = div(QRAFFLE_ASSET_RAFFLE_PROPOSAL_FEE, 2); - state.mut().epochRevenue += locals.proposalFeeHalf; - state.mut().epochAssetRaffleDaoBucket += (QRAFFLE_ASSET_RAFFLE_PROPOSAL_FEE - locals.proposalFeeHalf); - state.mut().totalAssetRaffleProposalFees += QRAFFLE_ASSET_RAFFLE_PROPOSAL_FEE; - state.mut().totalAssetRafflesCreated++; - - if (qpi.invocationReward() > (sint64)QRAFFLE_ASSET_RAFFLE_PROPOSAL_FEE) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)QRAFFLE_ASSET_RAFFLE_PROPOSAL_FEE); - } - - output.raffleIndex = locals.slot; - output.returnCode = QRAFFLE_SUCCESS; - locals.clog = AssetRaffleCreatedLogger{ - QRAFFLE_CONTRACT_INDEX, QRAFFLE_assetRaffleCreated, - locals.slot, qpi.invocator(), - input.reservePriceQu, input.entryTicketQu, - input.bundleSize, 0 - }; - LOG_INFO(locals.clog); - } - - // ── buyAssetRaffleTicket ─────────────────────────────────────────────────── - struct buyAssetRaffleTicket_locals - { - AssetRaffleInfo info; - AssetRaffleTicketLogger tlog; - Logger log; - BitArray participation; - Array slotIndexArr; - uint64 cost; - uint32 baseSlot; - uint32 existingTickets; - uint32 buyerSlot; - bit isNewBuyer; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(buyAssetRaffleTicket) - { - if (input.indexOfAssetRaffle >= state.get().numberOfActiveAssetRaffles) - { - if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.returnCode = QRAFFLE_INVALID_ASSET_RAFFLE; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidTokenRaffle, 0 }; - LOG_INFO(locals.log); - return; - } - if (input.numberOfTickets == 0 || input.numberOfTickets > QRAFFLE_MAX_TICKETS_PER_BUYER) - { - // Reject zero or absurd ticket counts up-front so the cost multiplication below cannot overflow. - if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.returnCode = QRAFFLE_INVALID_ENTRY_AMOUNT; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidEntryAmount, 0 }; - LOG_INFO(locals.log); - return; - } - locals.info = state.get().activeAssetRaffles.get(input.indexOfAssetRaffle); - // Overflow-safe cost: entryTicketQu ≤ 1e12 (QRAFFLE_MAX_ASSET_TICKET_AMOUNT) and tickets ≤ 100, - // so cost ≤ 1e14, safely under uint64 max (~1.8e19). totalTicketsPaidQu accumulates across - // up to 1024 buyers, max ~1e17, also safe. - locals.cost = locals.info.entryTicketQu * (uint64)input.numberOfTickets; - if (qpi.invocationReward() < (sint64)locals.cost) - { - if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.returnCode = QRAFFLE_INSUFFICIENT_FUND; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_insufficientQubic, 0 }; - LOG_INFO(locals.log); - return; - } - - locals.baseSlot = input.indexOfAssetRaffle * QRAFFLE_MAX_ASSET_TICKET_BUYERS; - locals.existingTickets = 0; - locals.isNewBuyer = 1; - locals.buyerSlot = locals.baseSlot + locals.info.numberOfBuyers; - - state.get().assetRaffleParticipation.get(qpi.invocator(), locals.participation); - if (locals.participation.get(input.indexOfAssetRaffle)) - { - // Returning buyer: resolve slot via index map (O(1)). - locals.isNewBuyer = 0; - state.get().assetRaffleBuyerSlotIndex.get(qpi.invocator(), locals.slotIndexArr); - locals.buyerSlot = locals.baseSlot + (uint32)locals.slotIndexArr.get(input.indexOfAssetRaffle); - locals.existingTickets = state.get().activeAssetRaffleBuyerTickets.get(locals.buyerSlot); - } - - if (locals.existingTickets + input.numberOfTickets > QRAFFLE_MAX_TICKETS_PER_BUYER) - { - if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.returnCode = QRAFFLE_TICKET_LIMIT_REACHED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_ticketLimitReached, 0 }; - LOG_INFO(locals.log); - return; - } - - if (locals.isNewBuyer) - { - if (locals.info.numberOfBuyers >= QRAFFLE_MAX_ASSET_TICKET_BUYERS) - { - if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.returnCode = QRAFFLE_ASSET_RAFFLE_FULL; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_assetRaffleFull, 0 }; - LOG_INFO(locals.log); - return; - } - state.mut().activeAssetRaffleBuyers.set(locals.buyerSlot, qpi.invocator()); - // Record slot position so future purchases skip the buyer-list scan. - state.get().assetRaffleBuyerSlotIndex.get(qpi.invocator(), locals.slotIndexArr); - locals.slotIndexArr.set(input.indexOfAssetRaffle, (uint16)locals.info.numberOfBuyers); - state.mut().assetRaffleBuyerSlotIndex.set(qpi.invocator(), locals.slotIndexArr); - locals.participation.set(input.indexOfAssetRaffle, 1); - state.mut().assetRaffleParticipation.set(qpi.invocator(), locals.participation); - locals.info.numberOfBuyers++; - } - - state.mut().activeAssetRaffleBuyerTickets.set(locals.buyerSlot, locals.existingTickets + input.numberOfTickets); - locals.info.totalTickets += input.numberOfTickets; - locals.info.totalTicketsPaidQu += locals.cost; - state.mut().activeAssetRaffles.set(input.indexOfAssetRaffle, locals.info); - - if (qpi.invocationReward() > (sint64)locals.cost) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)locals.cost); - } - - output.ticketsBought = input.numberOfTickets; - output.returnCode = QRAFFLE_SUCCESS; - locals.tlog = AssetRaffleTicketLogger{ - QRAFFLE_CONTRACT_INDEX, QRAFFLE_assetRaffleTicketBought, - input.indexOfAssetRaffle, qpi.invocator(), - input.numberOfTickets, locals.cost, 0 - }; - LOG_INFO(locals.tlog); - } - - // ── cancelAssetRaffle ────────────────────────────────────────────────────── - struct cancelAssetRaffle_locals - { - AssetRaffleInfo info; - AssetRaffleItem item; - AssetRaffleInfo lastInfo; - AssetRaffleItem lastItem; - Array slotIndexArr; - BitArray participation; - Logger log; - id movedBuyer; - uint8 creatorCount; - uint32 lastSlot; - uint32 i; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(cancelAssetRaffle) - { - if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - - if (input.indexOfAssetRaffle >= state.get().numberOfActiveAssetRaffles) - { - output.returnCode = QRAFFLE_INVALID_ASSET_RAFFLE; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_invalidTokenRaffle, 0 }; - LOG_INFO(locals.log); - return; - } - locals.info = state.get().activeAssetRaffles.get(input.indexOfAssetRaffle); - if (locals.info.creator != qpi.invocator()) - { - output.returnCode = QRAFFLE_CANCEL_NOT_ALLOWED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_cancelNotAllowed, 0 }; - LOG_INFO(locals.log); - return; - } - if (locals.info.numberOfBuyers > 0) - { - output.returnCode = QRAFFLE_CANCEL_NOT_ALLOWED; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_cancelNotAllowed, 0 }; - LOG_INFO(locals.log); - return; - } - - for (locals.i = 0; locals.i < locals.info.bundleSize; locals.i++) - { - locals.item = state.get().activeAssetRaffleItems.get(input.indexOfAssetRaffle * QRAFFLE_MAX_ASSETS_PER_BUNDLE + locals.i); - qpi.transferShareOwnershipAndPossession( - locals.item.asset.assetName, locals.item.asset.issuer, - SELF, SELF, - locals.item.numberOfShares, qpi.invocator()); - } - - // Swap-and-pop: fill the vacated slot with the last active raffle. - // Three parallel arrays must be kept in sync: raffle info, bundle items, and buyer lists. - locals.lastSlot = state.get().numberOfActiveAssetRaffles - 1; - if (input.indexOfAssetRaffle < locals.lastSlot) - { - locals.lastInfo = state.get().activeAssetRaffles.get(locals.lastSlot); - state.mut().activeAssetRaffles.set(input.indexOfAssetRaffle, locals.lastInfo); - - for (locals.i = 0; locals.i < locals.lastInfo.bundleSize; locals.i++) - { - locals.lastItem = state.get().activeAssetRaffleItems.get(locals.lastSlot * QRAFFLE_MAX_ASSETS_PER_BUNDLE + locals.i); - state.mut().activeAssetRaffleItems.set(input.indexOfAssetRaffle * QRAFFLE_MAX_ASSETS_PER_BUNDLE + locals.i, locals.lastItem); - } - - // Cancelled raffle already has numberOfBuyers==0, so the destination region is safe to overwrite. - for (locals.i = 0; locals.i < locals.lastInfo.numberOfBuyers; locals.i++) - { - locals.movedBuyer = state.get().activeAssetRaffleBuyers.get(locals.lastSlot * QRAFFLE_MAX_ASSET_TICKET_BUYERS + locals.i); - state.mut().activeAssetRaffleBuyers.set( - input.indexOfAssetRaffle * QRAFFLE_MAX_ASSET_TICKET_BUYERS + locals.i, - locals.movedBuyer); - state.mut().activeAssetRaffleBuyerTickets.set( - input.indexOfAssetRaffle * QRAFFLE_MAX_ASSET_TICKET_BUYERS + locals.i, - state.get().activeAssetRaffleBuyerTickets.get(locals.lastSlot * QRAFFLE_MAX_ASSET_TICKET_BUYERS + locals.i)); - // Update slot-index map: raffle index changed from lastSlot to indexOfAssetRaffle; - // relative position within the buyer region is preserved. - state.get().assetRaffleBuyerSlotIndex.get(locals.movedBuyer, locals.slotIndexArr); - locals.slotIndexArr.set(input.indexOfAssetRaffle, locals.slotIndexArr.get(locals.lastSlot)); - locals.slotIndexArr.set(locals.lastSlot, 0xFFFF); - state.mut().assetRaffleBuyerSlotIndex.replace(locals.movedBuyer, locals.slotIndexArr); - - // Keep participation bitfield aligned with moved raffle index. - state.get().assetRaffleParticipation.get(locals.movedBuyer, locals.participation); - locals.participation.set(input.indexOfAssetRaffle, 1); - locals.participation.set(locals.lastSlot, 0); - state.mut().assetRaffleParticipation.replace(locals.movedBuyer, locals.participation); - } - } - state.mut().numberOfActiveAssetRaffles--; - - // Decrement per-creator counter so the creator can replace the cancelled raffle - // within the same epoch. The 500K proposal fee is *not* refunded (anti-spam). - locals.creatorCount = 0; - if (state.get().assetRafflesPerCreator.contains(qpi.invocator())) - { - state.get().assetRafflesPerCreator.get(qpi.invocator(), locals.creatorCount); - if (locals.creatorCount > 0) - { - state.mut().assetRafflesPerCreator.set(qpi.invocator(), (uint8)(locals.creatorCount - 1)); - } - } - - output.returnCode = QRAFFLE_SUCCESS; - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_assetRaffleCancelled, 0 }; - LOG_INFO(locals.log); - } - - struct getRegisters_locals - { - id user; - sint64 idx; - uint32 i; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(getRegisters) - { - if (input.limit > 20) - { - output.returnCode = QRAFFLE_INVALID_OFFSET_OR_LIMIT; - return ; - } - if (input.offset >= state.get().numberOfRegisters) - { - output.returnCode = QRAFFLE_SUCCESS; - return ; - } - locals.idx = state.get().registers.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) - { - locals.user = state.get().registers.key(locals.idx); - if (locals.i >= input.offset && locals.i < input.offset + input.limit) - { - if (locals.i - input.offset == 0) - { - output.register1 = locals.user; - } - else if (locals.i - input.offset == 1) - { - output.register2 = locals.user; - } - else if (locals.i - input.offset == 2) - { - output.register3 = locals.user; - } - else if (locals.i - input.offset == 3) - { - output.register4 = locals.user; - } - else if (locals.i - input.offset == 4) - { - output.register5 = locals.user; - } - else if (locals.i - input.offset == 5) - { - output.register6 = locals.user; - } - else if (locals.i - input.offset == 6) - { - output.register7 = locals.user; - } - else if (locals.i - input.offset == 7) - { - output.register8 = locals.user; - } - else if (locals.i - input.offset == 8) - { - output.register9 = locals.user; - } - else if (locals.i - input.offset == 9) - { - output.register10 = locals.user; - } - else if (locals.i - input.offset == 10) - { - output.register11 = locals.user; - } - else if (locals.i - input.offset == 11) - { - output.register12 = locals.user; - } - else if (locals.i - input.offset == 12) - { - output.register13 = locals.user; - } - else if (locals.i - input.offset == 13) - { - output.register14 = locals.user; - } - else if (locals.i - input.offset == 14) - { - output.register15 = locals.user; - } - else if (locals.i - input.offset == 15) - { - output.register16 = locals.user; - } - else if (locals.i - input.offset == 16) - { - output.register17 = locals.user; - } - else if (locals.i - input.offset == 17) - { - output.register18 = locals.user; - } - else if (locals.i - input.offset == 18) - { - output.register19 = locals.user; - } - else if (locals.i - input.offset == 19) - { - output.register20 = locals.user; - } - } - if (locals.i >= input.offset + input.limit) - { - break; - } - locals.i++; - locals.idx = state.get().registers.nextElementIndex(locals.idx); - } - output.returnCode = QRAFFLE_SUCCESS; - } - - PUBLIC_FUNCTION(getAnalytics) - { - output.currentQuRaffleAmount = state.get().qREAmount; - output.totalBurnAmount = state.get().totalBurnAmount; - output.totalCharityAmount = state.get().totalCharityAmount; - output.totalShareholderAmount = state.get().totalShareholderAmount; - output.totalRegisterAmount = state.get().totalRegisterAmount; - output.totalFeeAmount = state.get().totalFeeAmount; - output.totalWinnerAmount = state.get().totalWinnerAmount; - output.largestWinnerAmount = state.get().largestWinnerAmount; - output.numberOfRegisters = state.get().numberOfRegisters; - output.numberOfProposals = state.get().numberOfProposals; - output.numberOfQuRaffleMembers = state.get().numberOfQuRaffleMembers; - output.numberOfActiveTokenRaffle = state.get().numberOfActiveTokenRaffle; - output.numberOfEndedTokenRaffle = state.get().numberOfEndedTokenRaffle; - output.numberOfEntryAmountSubmitted = state.get().numberOfEntryAmountSubmitted; - output.returnCode = QRAFFLE_SUCCESS; - } - - PUBLIC_FUNCTION(getActiveProposal) - { - if (input.indexOfProposal >= state.get().numberOfProposals) - { - output.returnCode = QRAFFLE_INVALID_PROPOSAL; - return ; - } - output.tokenName = state.get().proposals.get(input.indexOfProposal).token.assetName; - output.tokenIssuer = state.get().proposals.get(input.indexOfProposal).token.issuer; - output.proposer = state.get().proposals.get(input.indexOfProposal).proposer; - output.entryAmount = state.get().proposals.get(input.indexOfProposal).entryAmount; - output.nYes = state.get().proposals.get(input.indexOfProposal).nYes; - output.nNo = state.get().proposals.get(input.indexOfProposal).nNo; - output.returnCode = QRAFFLE_SUCCESS; - } - - PUBLIC_FUNCTION(getEndedTokenRaffle) - { - if (input.indexOfRaffle >= state.get().numberOfEndedTokenRaffle) - { - output.returnCode = QRAFFLE_INVALID_TOKEN_RAFFLE; - return ; - } - // Reject indices that have been overwritten by the ring buffer. - if (state.get().numberOfEndedTokenRaffle > QRAFFLE_MAX_TOKEN_RAFFLES - && input.indexOfRaffle < state.get().numberOfEndedTokenRaffle - QRAFFLE_MAX_TOKEN_RAFFLES) - { - output.returnCode = QRAFFLE_INVALID_TOKEN_RAFFLE; - return ; - } - output.epochWinner = state.get().tokenRaffle.get(input.indexOfRaffle).epochWinner; - output.tokenName = state.get().tokenRaffle.get(input.indexOfRaffle).token.assetName; - output.tokenIssuer = state.get().tokenRaffle.get(input.indexOfRaffle).token.issuer; - output.entryAmount = state.get().tokenRaffle.get(input.indexOfRaffle).entryAmount; - output.numberOfMembers = state.get().tokenRaffle.get(input.indexOfRaffle).numberOfMembers; - output.winnerIndex = state.get().tokenRaffle.get(input.indexOfRaffle).winnerIndex; - output.epoch = state.get().tokenRaffle.get(input.indexOfRaffle).epoch; - output.returnCode = QRAFFLE_SUCCESS; - } - - struct getEpochRaffleIndexes_locals - { - uint32 ringStart; - uint32 ringEnd; - uint32 i; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(getEpochRaffleIndexes) - { - output.StartIndex = 0; - output.EndIndex = 0; - if (input.epoch > qpi.epoch()) - { - output.returnCode = QRAFFLE_INVALID_EPOCH; - return ; - } - if (input.epoch == qpi.epoch()) - { - output.StartIndex = 0; - output.EndIndex = state.get().numberOfActiveTokenRaffle; - output.returnCode = QRAFFLE_SUCCESS; - return ; - } - // Only scan the valid ring window to avoid re-reading overwritten slots. - locals.ringEnd = state.get().numberOfEndedTokenRaffle; - locals.ringStart = (locals.ringEnd > QRAFFLE_MAX_TOKEN_RAFFLES) ? (locals.ringEnd - QRAFFLE_MAX_TOKEN_RAFFLES) : 0; - for (locals.i = locals.ringStart; locals.i < locals.ringEnd; locals.i++) - { - if (state.get().tokenRaffle.get(locals.i).epoch == input.epoch) - { - output.StartIndex = locals.i; - break; - } - } - locals.i = locals.ringEnd; - while (locals.i > locals.ringStart) - { - locals.i--; - if (state.get().tokenRaffle.get(locals.i).epoch == input.epoch) - { - output.EndIndex = locals.i; - break; - } - } - output.returnCode = QRAFFLE_SUCCESS; - } - - PUBLIC_FUNCTION(getEndedQuRaffle) - { - // Note: indices are masked by the underlying Array (capacity is power-of-two), so - // any uint32 epoch value is safely bounded inside the QuRaffles ring. Slots that - // were never written return a zero-initialised QuRaffleInfo, which the caller can - // detect via numberOfMembers == 0 / epochWinner == NULL_ID. - output.epochWinner = state.get().QuRaffles.get(input.epoch).epochWinner; - output.receivedAmount = state.get().QuRaffles.get(input.epoch).receivedAmount; - output.entryAmount = state.get().QuRaffles.get(input.epoch).entryAmount; - output.numberOfMembers = state.get().QuRaffles.get(input.epoch).numberOfMembers; - output.winnerIndex = state.get().QuRaffles.get(input.epoch).winnerIndex; - output.numberOfDaoMembers = state.get().daoMemberCount.get(input.epoch); - output.returnCode = QRAFFLE_SUCCESS; - } - - PUBLIC_FUNCTION(getActiveTokenRaffle) - { - if (input.indexOfTokenRaffle >= state.get().numberOfActiveTokenRaffle) - { - output.returnCode = QRAFFLE_INVALID_TOKEN_RAFFLE; - return ; - } - output.tokenName = state.get().activeTokenRaffle.get(input.indexOfTokenRaffle).token.assetName; - output.tokenIssuer = state.get().activeTokenRaffle.get(input.indexOfTokenRaffle).token.issuer; - output.entryAmount = state.get().activeTokenRaffle.get(input.indexOfTokenRaffle).entryAmount; - output.numberOfMembers = state.get().numberOfTokenRaffleMembers.get(input.indexOfTokenRaffle); - output.returnCode = QRAFFLE_SUCCESS; - } - - PUBLIC_FUNCTION(getQuRaffleEntryAmountPerUser) - { - if (state.get().quRaffleEntryAmount.contains(input.user) == 0) - { - output.entryAmount = 0; - output.returnCode = QRAFFLE_USER_NOT_FOUND; - } - else - { - state.get().quRaffleEntryAmount.get(input.user, output.entryAmount); - output.returnCode = QRAFFLE_SUCCESS; - } - } - - struct getQuRaffleEntryAverageAmount_locals - { - uint64 entryAmount; - uint64 totalEntryAmount; - sint64 idx; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(getQuRaffleEntryAverageAmount) - { - locals.entryAmount = 0; - locals.totalEntryAmount = 0; - locals.idx = state.get().quRaffleEntryAmount.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) - { - locals.entryAmount = state.get().quRaffleEntryAmount.value(locals.idx); - locals.totalEntryAmount += locals.entryAmount; - locals.idx = state.get().quRaffleEntryAmount.nextElementIndex(locals.idx); - } - if (state.get().numberOfEntryAmountSubmitted > 0) - { - output.entryAverageAmount = div(locals.totalEntryAmount, state.get().numberOfEntryAmountSubmitted); - } - else - { - output.entryAverageAmount = 0; - } - output.returnCode = QRAFFLE_SUCCESS; - } - - // ── Asset Raffle view functions ──────────────────────────────────────────── - - struct getActiveAssetRaffle_locals - { - AssetRaffleInfo info; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(getActiveAssetRaffle) - { - if (input.indexOfAssetRaffle >= state.get().numberOfActiveAssetRaffles) - { - output.returnCode = QRAFFLE_INVALID_ASSET_RAFFLE; - return; - } - locals.info = state.get().activeAssetRaffles.get(input.indexOfAssetRaffle); - output.creator = locals.info.creator; - output.reservePriceQu = locals.info.reservePriceQu; - output.entryTicketQu = locals.info.entryTicketQu; - output.totalTicketsPaidQu = locals.info.totalTicketsPaidQu; - output.numberOfBuyers = locals.info.numberOfBuyers; - output.totalTickets = locals.info.totalTickets; - output.bundleSize = locals.info.bundleSize; - output.epoch = locals.info.epoch; - output.returnCode = QRAFFLE_SUCCESS; - } - - struct getActiveAssetRaffleBundleItem_locals - { - AssetRaffleInfo info; - AssetRaffleItem item; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(getActiveAssetRaffleBundleItem) - { - if (input.indexOfAssetRaffle >= state.get().numberOfActiveAssetRaffles) - { - output.returnCode = QRAFFLE_INVALID_ASSET_RAFFLE; - return; - } - locals.info = state.get().activeAssetRaffles.get(input.indexOfAssetRaffle); - if (input.itemIndex >= locals.info.bundleSize) - { - output.returnCode = QRAFFLE_INVALID_BUNDLE; - return; - } - locals.item = state.get().activeAssetRaffleItems.get( - input.indexOfAssetRaffle * QRAFFLE_MAX_ASSETS_PER_BUNDLE + input.itemIndex); - output.assetIssuer = locals.item.asset.issuer; - output.assetName = locals.item.asset.assetName; - output.numberOfShares = locals.item.numberOfShares; - output.returnCode = QRAFFLE_SUCCESS; - } - - struct getActiveAssetRaffleBuyer_locals - { - AssetRaffleInfo info; - uint32 slot; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(getActiveAssetRaffleBuyer) - { - if (input.indexOfAssetRaffle >= state.get().numberOfActiveAssetRaffles) - { - output.returnCode = QRAFFLE_INVALID_ASSET_RAFFLE; - return; - } - locals.info = state.get().activeAssetRaffles.get(input.indexOfAssetRaffle); - if (input.buyerIndex >= locals.info.numberOfBuyers) - { - output.returnCode = QRAFFLE_INVALID_OFFSET_OR_LIMIT; - return; - } - locals.slot = input.indexOfAssetRaffle * QRAFFLE_MAX_ASSET_TICKET_BUYERS + input.buyerIndex; - output.buyer = state.get().activeAssetRaffleBuyers.get(locals.slot); - output.ticketCount = state.get().activeAssetRaffleBuyerTickets.get(locals.slot); - output.returnCode = QRAFFLE_SUCCESS; - } - - struct getEndedAssetRaffle_locals - { - EndedAssetRaffleInfo r; - uint32 slot; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(getEndedAssetRaffle) - { - if (input.indexOfRaffle >= state.get().numberOfEndedAssetRaffles) - { - output.returnCode = QRAFFLE_INVALID_ASSET_RAFFLE; - return; - } - // Reject indices that have been overwritten by the ring buffer. - if (state.get().numberOfEndedAssetRaffles > QRAFFLE_MAX_ENDED_ASSET_RAFFLES - && input.indexOfRaffle < state.get().numberOfEndedAssetRaffles - QRAFFLE_MAX_ENDED_ASSET_RAFFLES) - { - output.returnCode = QRAFFLE_INVALID_ASSET_RAFFLE; - return; - } - locals.slot = mod(input.indexOfRaffle, QRAFFLE_MAX_ENDED_ASSET_RAFFLES); - locals.r = state.get().endedAssetRaffles.get(locals.slot); - output.creator = locals.r.creator; - output.epochWinner = locals.r.epochWinner; - output.reservePriceQu = locals.r.reservePriceQu; - output.entryTicketQu = locals.r.entryTicketQu; - output.grossPoolQu = locals.r.grossPoolQu; - output.creatorPaidQu = locals.r.creatorPaidQu; - output.totalTickets = locals.r.totalTickets; - output.numberOfBuyers = locals.r.numberOfBuyers; - output.bundleSize = locals.r.bundleSize; - output.epoch = locals.r.epoch; - output.reserveMet = locals.r.reserveMet; - output.returnCode = QRAFFLE_SUCCESS; - } - - PUBLIC_FUNCTION(getAssetRaffleAnalytics) - { - output.totalAssetRaffleProposalFees = state.get().totalAssetRaffleProposalFees; - output.totalAssetRaffleCreatorPaid = state.get().totalAssetRaffleCreatorPaid; - output.totalAssetRaffleRefunded = state.get().totalAssetRaffleRefunded; - output.numberOfActiveAssetRaffles = state.get().numberOfActiveAssetRaffles; - output.numberOfEndedAssetRaffles = state.get().numberOfEndedAssetRaffles; - output.totalAssetRafflesCreated = state.get().totalAssetRafflesCreated; - output.totalAssetRafflesSucceeded = state.get().totalAssetRafflesSucceeded; - output.totalAssetRafflesFailed = state.get().totalAssetRafflesFailed; - output.returnCode = QRAFFLE_SUCCESS; - } - - REGISTER_USER_FUNCTIONS_AND_PROCEDURES() - { - REGISTER_USER_FUNCTION(getRegisters, 1); - REGISTER_USER_FUNCTION(getAnalytics, 2); - REGISTER_USER_FUNCTION(getActiveProposal, 3); - REGISTER_USER_FUNCTION(getEndedTokenRaffle, 4); - REGISTER_USER_FUNCTION(getEndedQuRaffle, 5); - REGISTER_USER_FUNCTION(getActiveTokenRaffle, 6); - REGISTER_USER_FUNCTION(getEpochRaffleIndexes, 7); - REGISTER_USER_FUNCTION(getQuRaffleEntryAmountPerUser, 8); - REGISTER_USER_FUNCTION(getQuRaffleEntryAverageAmount, 9); - // Asset Raffle view functions - REGISTER_USER_FUNCTION(getActiveAssetRaffle, 10); - REGISTER_USER_FUNCTION(getActiveAssetRaffleBundleItem, 11); - REGISTER_USER_FUNCTION(getActiveAssetRaffleBuyer, 12); - REGISTER_USER_FUNCTION(getEndedAssetRaffle, 13); - REGISTER_USER_FUNCTION(getAssetRaffleAnalytics, 14); - - REGISTER_USER_PROCEDURE(registerInSystem, 1); - REGISTER_USER_PROCEDURE(logoutInSystem, 2); - REGISTER_USER_PROCEDURE(submitEntryAmount, 3); - REGISTER_USER_PROCEDURE(submitProposal, 4); - REGISTER_USER_PROCEDURE(voteInProposal, 5); - REGISTER_USER_PROCEDURE(depositInQuRaffle, 6); - REGISTER_USER_PROCEDURE(depositInTokenRaffle, 7); - REGISTER_USER_PROCEDURE(TransferShareManagementRights, 8); - // Asset Raffle procedures - REGISTER_USER_PROCEDURE(createAssetRaffle, 9); - REGISTER_USER_PROCEDURE(buyAssetRaffleTicket, 10); - REGISTER_USER_PROCEDURE(cancelAssetRaffle, 11); - } - - INITIALIZE() - { - state.mut().qREAmount = QRAFFLE_DEFAULT_QRAFFLE_AMOUNT; - state.mut().charityAddress = ID(_D, _P, _Q, _R, _L, _S, _Z, _S, _S, _C, _X, _I, _Y, _F, _I, _Q, _G, _B, _F, _B, _X, _X, _I, _S, _D, _D, _E, _B, _E, _G, _Q, _N, _W, _N, _T, _Q, _U, _E, _I, _F, _S, _C, _U, _W, _G, _H, _V, _X, _J, _P, _L, _F, _G, _M, _Y, _D); - state.mut().initialRegister1 = ID(_I, _L, _N, _J, _X, _V, _H, _A, _U, _X, _D, _G, _G, _B, _T, _T, _U, _O, _I, _T, _O, _Q, _G, _P, _A, _Y, _U, _C, _F, _T, _N, _C, _P, _X, _D, _K, _O, _C, _P, _U, _O, _C, _D, _O, _T, _P, _U, _W, _X, _B, _I, _G, _R, _V, _Q, _D); - state.mut().initialRegister2 = ID(_L, _S, _D, _A, _A, _C, _L, _X, _X, _G, _I, _P, _G, _G, _L, _S, _O, _C, _L, _M, _V, _A, _Y, _L, _N, _T, _G, _D, _V, _B, _N, _O, _S, _S, _Y, _E, _Q, _D, _R, _K, _X, _D, _Y, _W, _B, _C, _G, _J, _I, _K, _C, _M, _Z, _K, _M, _F); - state.mut().initialRegister3 = ID(_G, _H, _G, _R, _L, _W, _S, _X, _Z, _X, _W, _D, _A, _A, _O, _M, _T, _X, _Q, _Y, _U, _P, _R, _L, _P, _N, _K, _C, _W, _G, _H, _A, _E, _F, _I, _R, _J, _I, _Z, _A, _K, _C, _A, _U, _D, _G, _N, _M, _C, _D, _E, _Q, _R, _O, _Q, _B); - state.mut().initialRegister4 = ID(_E, _U, _O, _N, _A, _Z, _J, _U, _A, _G, _V, _D, _C, _E, _I, _B, _A, _H, _J, _E, _T, _G, _U, _U, _H, _M, _N, _D, _J, _C, _S, _E, _T, _T, _Q, _V, _G, _Y, _F, _H, _M, _D, _P, _X, _T, _A, _L, _D, _Y, _U, _V, _E, _P, _F, _C, _A); - state.mut().initialRegister5 = ID(_S, _L, _C, _J, _C, _C, _U, _X, _G, _K, _N, _V, _A, _D, _F, _B, _E, _A, _Y, _V, _L, _S, _O, _B, _Z, _P, _A, _B, _H, _K, _S, _G, _M, _H, _W, _H, _S, _H, _G, _G, _B, _A, _P, _J, _W, _F, _V, _O, _K, _Z, _J, _P, _F, _L, _X, _D); - state.mut().QXMRIssuer = ID(_Q, _X, _M, _R, _T, _K, _A, _I, _I, _G, _L, _U, _R, _E, _P, _I, _Q, _P, _C, _M, _H, _C, _K, _W, _S, _I, _P, _D, _T, _U, _Y, _F, _C, _F, _N, _Y, _X, _Q, _L, _T, _E, _C, _S, _U, _J, _V, _Y, _E, _M, _M, _D, _E, _L, _B, _M, _D); - state.mut().feeAddress = ID(_H, _H, _R, _L, _C, _Z, _Q, _V, _G, _O, _M, _G, _X, _G, _F, _P, _H, _T, _R, _H, _H, _D, _W, _A, _E, _U, _X, _C, _N, _D, _L, _Z, _S, _Z, _J, _R, _M, _O, _R, _J, _K, _A, _I, _W, _S, _U, _Y, _R, _N, _X, _I, _H, _H, _O, _W, _D); - - state.mut().registers.set(state.get().initialRegister1, 0); - state.mut().registers.set(state.get().initialRegister2, 0); - state.mut().registers.set(state.get().initialRegister3, 0); - state.mut().registers.set(state.get().initialRegister4, 0); - state.mut().registers.set(state.get().initialRegister5, 0); - state.mut().numberOfRegisters = 5; - } - - struct END_EPOCH_locals - { - ProposalInfo proposal; - QuRaffleInfo qraffle; - TokenRaffleInfo tRaffle; - ActiveTokenRaffleInfo acTokenRaffle; - AssetPossessionIterator iter; - Asset QraffleAsset; - id digest, computerDigest, winner, shareholder, baseSeed, raffleSeed; - sint64 idx; - sint64 sharesHeld; - sint64 perShare; - sint64 transferResult; - uint64 sumOfEntryAmountSubmitted, r, winnerRevenue, burnAmount, charityRevenue, shareholderRevenue, registerRevenue, fee, oneShareholderRev; - uint64 tokenPool; - uint64 shareholderPerShareUnit; - uint64 registerPerShareUnit; - uint64 actualShareholderTotal; - uint64 actualRegisterTotal; - uint64 qxmrPerShare; - uint64 qxmrDistributedTotal; - uint64 qxmrContractBalance; - uint32 i, j, winnerIndex; - Logger log; - EmptyTokenRaffleLogger emptyTokenRafflelog; - EndEpochLogger endEpochLog; - RevenueLogger revenueLog; - TokenRaffleLogger tokenRaffleLog; - ProposalLogger proposalLog; - // Asset raffle settlement locals - AssetRaffleInfo arInfo; - AssetRaffleItem arItem; - EndedAssetRaffleInfo arEnded; - AssetRaffleEndedLogger arLog; - uint64 arGross; - uint64 arCreatorPay; - uint64 arBurn; - uint64 arCharity; - uint64 arShareholderRev; - uint64 arRegisterRev; - uint64 arFee; - uint64 arShareholderPerShare; - uint64 arRegisterPerShare; - uint64 arRegisterPerShareActual; - uint64 arRegisterBucket; // accumulated register share across all successful asset raffles - uint64 arRegisterBucketPerReg; // per-register amount distributed after the settlement loop - uint64 arDaoBucketPerRegister; - uint64 arTicketAcc; - uint64 arRefund; // per-buyer refund amount (used in reserve-missed path) - uint32 arI; - uint32 arJ; - uint32 arBuyerSlot; - uint32 arWinnerIndex; - uint32 arEndedIdx; // ring-buffer slot (masked) the settled raffle is written to - uint32 arEndedGlobalIdx; // monotonic global index callers pass to getEndedAssetRaffle - bit arReserveMet; - }; - - END_EPOCH_WITH_LOCALS() - { - // Distribute logout-fee revenue to shareholders. - locals.oneShareholderRev = div(state.get().epochRevenue, NUMBER_OF_COMPUTORS); - if (locals.oneShareholderRev > 0) - { - qpi.distributeDividends(locals.oneShareholderRev); - state.mut().epochRevenue -= locals.oneShareholderRev * NUMBER_OF_COMPUTORS; - } - - // RNG seed: XOR of prevSpectrumDigest and prevComputerDigest, hashed with K12. - // Tick-level inputs are excluded so a computor cannot grind the seed. - locals.digest = qpi.getPrevSpectrumDigest(); - locals.computerDigest = qpi.getPrevComputerDigest(); - locals.baseSeed = qpi.K12(m256i( - locals.digest.u64._0 ^ locals.computerDigest.u64._0, - locals.digest.u64._1 ^ locals.computerDigest.u64._1, - locals.digest.u64._2 ^ locals.computerDigest.u64._2, - locals.digest.u64._3 ^ locals.computerDigest.u64._3)); - - // Asset descriptor reused by the QXMR distribution and per-token-raffle shareholder - // payout loops below; both iterate QRAFFLE_ASSET possessors directly via locals.iter. - locals.QraffleAsset.assetName = QRAFFLE_ASSET_NAME; - locals.QraffleAsset.issuer = NULL_ID; - - if (state.get().numberOfQuRaffleMembers > 0) - { - // Pick winner. - locals.raffleSeed = qpi.K12(m256i(locals.baseSeed.u64._0, locals.baseSeed.u64._1, locals.baseSeed.u64._2, locals.baseSeed.u64._3)); - locals.r = locals.raffleSeed.u64._0; - locals.winnerIndex = (uint32)mod(locals.r, state.get().numberOfQuRaffleMembers * 1ull); - locals.winner = state.get().quRaffleMembers.get(locals.winnerIndex); - - // Calculate fee distributions. - locals.tokenPool = state.get().qREAmount * state.get().numberOfQuRaffleMembers; - locals.burnAmount = div(locals.tokenPool * QRAFFLE_BURN_FEE, 100); - locals.charityRevenue = div(locals.tokenPool * QRAFFLE_CHARITY_FEE, 100); - locals.shareholderRevenue = div(locals.tokenPool * QRAFFLE_SHAREHOLDER_FEE, 100); - locals.registerRevenue = div(locals.tokenPool * QRAFFLE_REGISTER_FEE, 100); - locals.fee = div(locals.tokenPool * QRAFFLE_FEE, 100); - // Round down per-share amounts; winner gets the remainder. - locals.shareholderPerShareUnit = div(locals.shareholderRevenue, NUMBER_OF_COMPUTORS); - locals.actualShareholderTotal = locals.shareholderPerShareUnit * NUMBER_OF_COMPUTORS; - locals.registerPerShareUnit = div(locals.registerRevenue, state.get().numberOfRegisters); - locals.actualRegisterTotal = locals.registerPerShareUnit * state.get().numberOfRegisters; - locals.winnerRevenue = locals.tokenPool - locals.burnAmount - locals.charityRevenue - locals.actualShareholderTotal - locals.actualRegisterTotal - locals.fee; - - locals.revenueLog = RevenueLogger{ - QRAFFLE_CONTRACT_INDEX, - QRAFFLE_revenueDistributed, - locals.burnAmount, - locals.charityRevenue, - locals.actualShareholderTotal, - locals.actualRegisterTotal, - locals.fee, - locals.winnerRevenue, - 0 - }; - LOG_INFO(locals.revenueLog); - - qpi.transfer(locals.winner, locals.winnerRevenue); - qpi.burn(locals.burnAmount); - qpi.transfer(state.get().charityAddress, locals.charityRevenue); - if (locals.shareholderPerShareUnit > 0) - { - qpi.distributeDividends(locals.shareholderPerShareUnit); - } - qpi.transfer(state.get().feeAddress, locals.fee); - - state.mut().totalBurnAmount += locals.burnAmount; - state.mut().totalCharityAmount += locals.charityRevenue; - state.mut().totalShareholderAmount += locals.actualShareholderTotal; - state.mut().totalRegisterAmount += locals.actualRegisterTotal; - state.mut().totalFeeAmount += locals.fee; - state.mut().totalWinnerAmount += locals.winnerRevenue; - if (locals.winnerRevenue > state.get().largestWinnerAmount) - { - state.mut().largestWinnerAmount = locals.winnerRevenue; - } - - if (locals.registerPerShareUnit > 0) - { - locals.idx = state.get().registers.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) - { - qpi.transfer(state.get().registers.key(locals.idx), locals.registerPerShareUnit); - locals.idx = state.get().registers.nextElementIndex(locals.idx); - } - } - - locals.qraffle.epochWinner = locals.winner; - locals.qraffle.receivedAmount = locals.winnerRevenue; - locals.qraffle.entryAmount = state.get().qREAmount; - locals.qraffle.numberOfMembers = state.get().numberOfQuRaffleMembers; - locals.qraffle.winnerIndex = locals.winnerIndex; - state.mut().QuRaffles.set(qpi.epoch(), locals.qraffle); - - locals.endEpochLog = EndEpochLogger{ - QRAFFLE_CONTRACT_INDEX, - QRAFFLE_revenueDistributed, - qpi.epoch(), - state.get().numberOfQuRaffleMembers, - locals.tokenPool, - locals.winnerRevenue, - locals.winnerIndex, - 0 - }; - LOG_INFO(locals.endEpochLog); - } - else - { - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_emptyQuRaffle, 0 }; - LOG_INFO(locals.log); - } - - locals.qxmrPerShare = div(state.get().epochQXMRRevenue, NUMBER_OF_COMPUTORS); - locals.qxmrDistributedTotal = 0; - if (locals.qxmrPerShare > 0) - { - locals.qxmrContractBalance = (uint64)qpi.numberOfPossessedShares(QRAFFLE_QXMR_ASSET_NAME, state.get().QXMRIssuer, SELF, SELF, SELF_INDEX, SELF_INDEX); - locals.iter.begin(locals.QraffleAsset); - while (!locals.iter.reachedEnd()) - { - locals.sharesHeld = locals.iter.numberOfPossessedShares(); - if (locals.sharesHeld > 0) - { - locals.shareholder = locals.iter.possessor(); - locals.perShare = (sint64)(locals.qxmrPerShare * (uint64)locals.sharesHeld); - if ((uint64)locals.perShare <= locals.qxmrContractBalance - locals.qxmrDistributedTotal) - { - locals.transferResult = qpi.transferShareOwnershipAndPossession(QRAFFLE_QXMR_ASSET_NAME, state.get().QXMRIssuer, SELF, SELF, locals.perShare, locals.shareholder); - if (locals.transferResult >= 0) - { - locals.qxmrDistributedTotal += (uint64)locals.perShare; - } - } - } - locals.iter.next(); - } - } - state.mut().epochQXMRRevenue -= locals.qxmrDistributedTotal; - - // Process each active token raffle. - for (locals.i = 0 ; locals.i < state.get().numberOfActiveTokenRaffle; locals.i++) - { - if (state.get().numberOfTokenRaffleMembers.get(locals.i) > 0) - { - locals.raffleSeed = qpi.K12(m256i(locals.baseSeed.u64._0, locals.baseSeed.u64._1, locals.baseSeed.u64._2, locals.baseSeed.u64._3 ^ ((uint64)locals.i + 1ULL))); - locals.r = locals.raffleSeed.u64._0; - locals.winnerIndex = (uint32)mod(locals.r, state.get().numberOfTokenRaffleMembers.get(locals.i) * 1ull); - locals.winner = state.get().tokenRaffleMemberSlots.get(locals.i * QRAFFLE_TOKEN_RAFFLE_SLOT_SIZE + locals.winnerIndex); - - locals.acTokenRaffle = state.get().activeTokenRaffle.get(locals.i); - - locals.tokenPool = locals.acTokenRaffle.entryAmount * state.get().numberOfTokenRaffleMembers.get(locals.i); - locals.burnAmount = div(locals.tokenPool * QRAFFLE_BURN_FEE, 100); - locals.charityRevenue = div(locals.tokenPool * QRAFFLE_CHARITY_FEE, 100); - locals.shareholderRevenue = div(locals.tokenPool * QRAFFLE_SHAREHOLDER_FEE, 100); - locals.registerRevenue = div(locals.tokenPool * QRAFFLE_REGISTER_FEE, 100); - locals.fee = div(locals.tokenPool * QRAFFLE_FEE, 100); - // Round down per-share amounts; winner gets the remainder. - locals.shareholderPerShareUnit = div(locals.shareholderRevenue, NUMBER_OF_COMPUTORS); - locals.actualShareholderTotal = locals.shareholderPerShareUnit * NUMBER_OF_COMPUTORS; - locals.registerPerShareUnit = div(locals.registerRevenue, state.get().numberOfRegisters); - locals.actualRegisterTotal = locals.registerPerShareUnit * state.get().numberOfRegisters; - locals.winnerRevenue = locals.tokenPool - locals.burnAmount - locals.charityRevenue - locals.actualShareholderTotal - locals.actualRegisterTotal - locals.fee; - - // Send winner share. Skip the whole raffle if this fails to prevent partial distribution. - locals.transferResult = qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.winnerRevenue, locals.winner); - if (locals.transferResult < 0) - { - // Winner transfer failed — skip raffle, leave funds for next epoch. - locals.emptyTokenRafflelog = EmptyTokenRaffleLogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_transferFailed, locals.i, 0 }; - LOG_INFO(locals.emptyTokenRafflelog); - state.mut().numberOfTokenRaffleMembers.set(locals.i, 0); - continue; - } - - // Burn shares (NULL_ID); fall back to charity if burn is rejected. - if (locals.burnAmount > 0) - { - locals.transferResult = qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.burnAmount, NULL_ID); - if (locals.transferResult < 0) - { - qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.burnAmount, state.get().charityAddress); - } - } - if (locals.charityRevenue > 0) - { - qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.charityRevenue, state.get().charityAddress); - } - if (locals.fee > 0) - { - qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, locals.fee, state.get().feeAddress); - } - - // Pay shareholders proportional to possessed shares. - if (locals.shareholderPerShareUnit > 0) - { - locals.iter.begin(locals.QraffleAsset); - while (!locals.iter.reachedEnd()) - { - locals.sharesHeld = locals.iter.numberOfPossessedShares(); - if (locals.sharesHeld > 0) - { - qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, (sint64)(locals.shareholderPerShareUnit * (uint64)locals.sharesHeld), locals.iter.possessor()); - } - locals.iter.next(); - } - } - - if (locals.registerPerShareUnit > 0) - { - locals.idx = state.get().registers.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) - { - qpi.transferShareOwnershipAndPossession(locals.acTokenRaffle.token.assetName, locals.acTokenRaffle.token.issuer, SELF, SELF, (sint64)locals.registerPerShareUnit, state.get().registers.key(locals.idx)); - locals.idx = state.get().registers.nextElementIndex(locals.idx); - } - } - - locals.tRaffle.epochWinner = locals.winner; - locals.tRaffle.token.assetName = locals.acTokenRaffle.token.assetName; - locals.tRaffle.token.issuer = locals.acTokenRaffle.token.issuer; - locals.tRaffle.entryAmount = locals.acTokenRaffle.entryAmount; - locals.tRaffle.numberOfMembers = state.get().numberOfTokenRaffleMembers.get(locals.i); - locals.tRaffle.winnerIndex = locals.winnerIndex; - locals.tRaffle.epoch = qpi.epoch(); - state.mut().tokenRaffle.set(state.get().numberOfEndedTokenRaffle, locals.tRaffle); - - locals.tokenRaffleLog = TokenRaffleLogger{ - QRAFFLE_CONTRACT_INDEX, - QRAFFLE_tokenRaffleEnded, - state.mut().numberOfEndedTokenRaffle++, - locals.acTokenRaffle.token.assetName, - state.get().numberOfTokenRaffleMembers.get(locals.i), - locals.acTokenRaffle.entryAmount, - locals.winnerIndex, - locals.winnerRevenue, - 0 - }; - LOG_INFO(locals.tokenRaffleLog); - - state.mut().numberOfTokenRaffleMembers.set(locals.i, 0); - } - else - { - locals.emptyTokenRafflelog = EmptyTokenRaffleLogger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_emptyTokenRaffle, locals.i, 0 }; - LOG_INFO(locals.emptyTokenRafflelog); - } - } - - // ── Asset Raffle settlement ──────────────────────────────────────────── - locals.arRegisterBucket = 0; - for (locals.arI = 0; locals.arI < state.get().numberOfActiveAssetRaffles; locals.arI++) - { - locals.arInfo = state.get().activeAssetRaffles.get(locals.arI); - locals.arGross = locals.arInfo.totalTicketsPaidQu; - - // Reserve test: gross * 80 >= reservePriceQu * 100 - locals.arReserveMet = (locals.arInfo.totalTickets > 0) - && (locals.arGross * 80ull >= locals.arInfo.reservePriceQu * 100ull); - - if (locals.arReserveMet) - { - // Weighted winner selection by ticket count. - locals.raffleSeed = qpi.K12(m256i( - locals.baseSeed.u64._0, locals.baseSeed.u64._1, - locals.baseSeed.u64._2, - locals.baseSeed.u64._3 ^ (0xA55E7000ULL + (uint64)locals.arI))); - locals.r = locals.raffleSeed.u64._0; - locals.r = mod(locals.r, (uint64)locals.arInfo.totalTickets); - locals.arTicketAcc = 0; - locals.arWinnerIndex = 0; - locals.winner = NULL_ID; - for (locals.arJ = 0; locals.arJ < locals.arInfo.numberOfBuyers; locals.arJ++) - { - locals.arBuyerSlot = locals.arI * QRAFFLE_MAX_ASSET_TICKET_BUYERS + locals.arJ; - locals.arTicketAcc += (uint64)state.get().activeAssetRaffleBuyerTickets.get(locals.arBuyerSlot); - if (locals.r < locals.arTicketAcc) - { - locals.arWinnerIndex = locals.arJ; - locals.winner = state.get().activeAssetRaffleBuyers.get(locals.arBuyerSlot); - break; - } - } - - // Transfer each bundle item to the winner. If an item fails, log and continue; - // we cannot pull assets back from a user's wallet, so the winner keeps whatever - // was delivered and the remaining escrowed items stay in the contract. - for (locals.arJ = 0; locals.arJ < locals.arInfo.bundleSize; locals.arJ++) - { - locals.arItem = state.get().activeAssetRaffleItems.get( - locals.arI * QRAFFLE_MAX_ASSETS_PER_BUNDLE + locals.arJ); - locals.transferResult = qpi.transferShareOwnershipAndPossession( - locals.arItem.asset.assetName, locals.arItem.asset.issuer, - SELF, SELF, - locals.arItem.numberOfShares, locals.winner); - if (locals.transferResult < 0) - { - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_assetRaffleBundleDeliveryFailed, 0 }; - LOG_INFO(locals.log); - } - } - - // Qu pool distribution: 80% to creator, 20% to fee buckets. - // Always executed when reserve is met, regardless of per-item delivery outcome. - // (Assets already delivered to winner; we cannot recall them from a user's wallet.) - locals.arBurn = div(locals.arGross * (uint64)QRAFFLE_BURN_FEE, 100ull); - locals.arCharity = div(locals.arGross * (uint64)QRAFFLE_CHARITY_FEE, 100ull); - locals.arShareholderRev = div(locals.arGross * (uint64)QRAFFLE_SHAREHOLDER_FEE, 100ull); - locals.arRegisterRev = div(locals.arGross * (uint64)QRAFFLE_REGISTER_FEE, 100ull); - locals.arFee = div(locals.arGross * (uint64)QRAFFLE_FEE, 100ull); - // Round down per-share amounts; all rounding dust goes to creator. - locals.arShareholderPerShare = div(locals.arShareholderRev, (uint64)NUMBER_OF_COMPUTORS); - locals.arRegisterPerShare = (state.get().numberOfRegisters > 0) - ? div(locals.arRegisterRev, (uint64)state.get().numberOfRegisters) - : 0; - locals.arRegisterPerShareActual = locals.arRegisterPerShare * (uint64)state.get().numberOfRegisters; - // Creator gets gross minus all deductions; rounding dust stays with creator. - locals.arCreatorPay = locals.arGross - - locals.arBurn - - locals.arCharity - - (locals.arShareholderPerShare * (uint64)NUMBER_OF_COMPUTORS) - - locals.arRegisterPerShareActual - - locals.arFee; - - qpi.transfer(locals.arInfo.creator, locals.arCreatorPay); - qpi.burn(locals.arBurn); - qpi.transfer(state.get().charityAddress, locals.arCharity); - if (locals.arShareholderPerShare > 0) - { - qpi.distributeDividends(locals.arShareholderPerShare); - } - qpi.transfer(state.get().feeAddress, locals.arFee); - // Accumulate register share; distributed in a single pass after the loop. - locals.arRegisterBucket += locals.arRegisterPerShareActual; - - state.mut().totalAssetRaffleCreatorPaid += locals.arCreatorPay; - state.mut().totalAssetRafflesSucceeded++; - } - - if (!locals.arReserveMet) - { - // Reserve not met: refund all Qu to buyers and return bundle to creator. - for (locals.arJ = 0; locals.arJ < locals.arInfo.numberOfBuyers; locals.arJ++) - { - locals.arBuyerSlot = locals.arI * QRAFFLE_MAX_ASSET_TICKET_BUYERS + locals.arJ; - locals.arRefund = (uint64)state.get().activeAssetRaffleBuyerTickets.get(locals.arBuyerSlot) * locals.arInfo.entryTicketQu; - qpi.transfer(state.get().activeAssetRaffleBuyers.get(locals.arBuyerSlot), locals.arRefund); - state.mut().totalAssetRaffleRefunded += locals.arRefund; - } - for (locals.arJ = 0; locals.arJ < locals.arInfo.bundleSize; locals.arJ++) - { - locals.arItem = state.get().activeAssetRaffleItems.get( - locals.arI * QRAFFLE_MAX_ASSETS_PER_BUNDLE + locals.arJ); - qpi.transferShareOwnershipAndPossession( - locals.arItem.asset.assetName, locals.arItem.asset.issuer, - SELF, SELF, - locals.arItem.numberOfShares, locals.arInfo.creator); - } - locals.arCreatorPay = 0; - locals.winner = NULL_ID; - locals.arWinnerIndex = 0; - state.mut().totalAssetRafflesFailed++; - } - - // Write to ended ring buffer. - // arEndedGlobalIdx is the monotonic logical index (pre-increment); this is the - // value callers pass to getEndedAssetRaffle. arEndedIdx is its ring-masked slot. - locals.arEndedGlobalIdx = state.get().numberOfEndedAssetRaffles; - locals.arEndedIdx = mod(locals.arEndedGlobalIdx, QRAFFLE_MAX_ENDED_ASSET_RAFFLES); - locals.arEnded.creator = locals.arInfo.creator; - locals.arEnded.epochWinner = locals.winner; - locals.arEnded.reservePriceQu = locals.arInfo.reservePriceQu; - locals.arEnded.entryTicketQu = locals.arInfo.entryTicketQu; - locals.arEnded.grossPoolQu = locals.arGross; - locals.arEnded.creatorPaidQu = locals.arCreatorPay; - locals.arEnded.totalTickets = locals.arInfo.totalTickets; - locals.arEnded.numberOfBuyers = locals.arInfo.numberOfBuyers; - locals.arEnded.bundleSize = locals.arInfo.bundleSize; - locals.arEnded.epoch = locals.arInfo.epoch; - locals.arEnded.reserveMet = locals.arReserveMet ? 1u : 0u; - state.mut().endedAssetRaffles.set(locals.arEndedIdx, locals.arEnded); - state.mut().numberOfEndedAssetRaffles++; - - locals.arLog = AssetRaffleEndedLogger{ - QRAFFLE_CONTRACT_INDEX, - locals.arReserveMet ? (uint32)QRAFFLE_assetRaffleSucceeded : (uint32)QRAFFLE_assetRaffleRefunded, - locals.arEndedGlobalIdx, - locals.arInfo.creator, - locals.winner, - locals.arGross, - locals.arCreatorPay, - locals.arReserveMet ? (uint8)1 : (uint8)0, - 0 - }; - LOG_INFO(locals.arLog); - } - - // Distribute accumulated register share from all successful asset raffles in one O(R) pass. - // Any integer-division remainder (up to numberOfRegisters-1 Qu) is folded into the DAO - // bucket below so the dust either pays out this same epoch via the DAO distribution that - // follows, or carries forward — never silently leaks into untracked contract balance. - if (locals.arRegisterBucket > 0 && state.get().numberOfRegisters > 0) - { - locals.arRegisterBucketPerReg = div(locals.arRegisterBucket, (uint64)state.get().numberOfRegisters); - if (locals.arRegisterBucketPerReg > 0) - { - locals.idx = state.get().registers.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) - { - qpi.transfer(state.get().registers.key(locals.idx), locals.arRegisterBucketPerReg); - locals.idx = state.get().registers.nextElementIndex(locals.idx); - } - } - state.mut().epochAssetRaffleDaoBucket += locals.arRegisterBucket - locals.arRegisterBucketPerReg * (uint64)state.get().numberOfRegisters; - } - - // Distribute DAO proposal-fee bucket evenly to registers. - // Subtract only what was actually paid out so the integer-division remainder carries - // forward to the next epoch instead of being silently dropped into contract balance. - if (state.get().epochAssetRaffleDaoBucket > 0 && state.get().numberOfRegisters > 0) - { - locals.arDaoBucketPerRegister = div(state.get().epochAssetRaffleDaoBucket, (uint64)state.get().numberOfRegisters); - if (locals.arDaoBucketPerRegister > 0) - { - locals.idx = state.get().registers.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) - { - qpi.transfer(state.get().registers.key(locals.idx), locals.arDaoBucketPerRegister); - locals.idx = state.get().registers.nextElementIndex(locals.idx); - } - } - state.mut().epochAssetRaffleDaoBucket -= locals.arDaoBucketPerRegister * (uint64)state.get().numberOfRegisters; - } - - // Reset asset raffle per-epoch state. - state.mut().numberOfActiveAssetRaffles = 0; - state.mut().assetRaffleParticipation.reset(); - state.mut().assetRaffleBuyerSlotIndex.reset(); - state.mut().assetRafflesPerCreator.reset(); - - // Calculate new qREAmount and log - locals.log = Logger{ QRAFFLE_CONTRACT_INDEX, QRAFFLE_revenueDistributed, 0 }; - LOG_INFO(locals.log); - - locals.sumOfEntryAmountSubmitted = 0; - locals.idx = state.get().quRaffleEntryAmount.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) - { - locals.sumOfEntryAmountSubmitted += state.get().quRaffleEntryAmount.value(locals.idx); - locals.idx = state.get().quRaffleEntryAmount.nextElementIndex(locals.idx); - } - if (state.get().numberOfEntryAmountSubmitted > 0) - { - state.mut().qREAmount = div(locals.sumOfEntryAmountSubmitted, state.get().numberOfEntryAmountSubmitted); - } - else - { - state.mut().qREAmount = QRAFFLE_DEFAULT_QRAFFLE_AMOUNT; - } - - state.mut().numberOfActiveTokenRaffle = 0; - - // Process approved proposals and create new token raffles - for (locals.i = 0 ; locals.i < state.get().numberOfProposals; locals.i++) - { - locals.proposal = state.get().proposals.get(locals.i); - - // Log proposal processing with detailed information - locals.proposalLog = ProposalLogger{ - QRAFFLE_CONTRACT_INDEX, - QRAFFLE_proposalSubmitted, - locals.i, - locals.proposal.proposer, - locals.proposal.nYes, - locals.proposal.nNo, - locals.proposal.token.assetName, - locals.proposal.entryAmount, - 0 - }; - LOG_INFO(locals.proposalLog); - - if (locals.proposal.nYes > locals.proposal.nNo) - { - locals.acTokenRaffle.token.assetName = locals.proposal.token.assetName; - locals.acTokenRaffle.token.issuer = locals.proposal.token.issuer; - locals.acTokenRaffle.entryAmount = locals.proposal.entryAmount; - - state.mut().activeTokenRaffle.set(state.mut().numberOfActiveTokenRaffle++, locals.acTokenRaffle); - } - } - - // Record DAO member count for this epoch before resetting per-epoch state. - state.mut().daoMemberCount.set(qpi.epoch(), state.get().numberOfRegisters); - - state.mut().numberOfVotedInProposal.setAll(0); - state.mut().tokenRaffleParticipation.reset(); - state.mut().proposalsPerProposer.reset(); - state.mut().quRaffleEntryAmount.reset(); - state.mut().voteParticipation.reset(); - state.mut().voteValues.reset(); - state.mut().numberOfEntryAmountSubmitted = 0; - state.mut().numberOfProposals = 0; - state.mut().numberOfQuRaffleMembers = 0; - state.mut().quRaffleMemberSet.reset(); - if (state.get().registers.needsCleanup()) { state.mut().registers.cleanup(); } - } - - MIGRATE() - { - copyMemory(state.mut().registers, oldState.registers); - copyMemory(state.mut().voteParticipation, oldState.voteParticipation); - copyMemory(state.mut().voteValues, oldState.voteValues); - copyMemory(state.mut().quRaffleMemberSet, oldState.quRaffleMemberSet); - copyMemory(state.mut().tokenRaffleParticipation, oldState.tokenRaffleParticipation); - copyMemory(state.mut().quRaffleEntryAmount, oldState.quRaffleEntryAmount); - copyMemory(state.mut().proposalsPerProposer, oldState.proposalsPerProposer); - - state.mut().proposals = oldState.proposals; - state.mut().numberOfVotedInProposal = oldState.numberOfVotedInProposal; - state.mut().quRaffleMembers = oldState.quRaffleMembers; - state.mut().activeTokenRaffle = oldState.activeTokenRaffle; - state.mut().tokenRaffleMemberSlots = oldState.tokenRaffleMemberSlots; - state.mut().numberOfTokenRaffleMembers = oldState.numberOfTokenRaffleMembers; - state.mut().QuRaffles = oldState.QuRaffles; - state.mut().tokenRaffle = oldState.tokenRaffle; - state.mut().daoMemberCount = oldState.daoMemberCount; - - state.mut().initialRegister1 = oldState.initialRegister1; - state.mut().initialRegister2 = oldState.initialRegister2; - state.mut().initialRegister3 = oldState.initialRegister3; - state.mut().initialRegister4 = oldState.initialRegister4; - state.mut().initialRegister5 = oldState.initialRegister5; - state.mut().charityAddress = oldState.charityAddress; - state.mut().feeAddress = oldState.feeAddress; - state.mut().QXMRIssuer = oldState.QXMRIssuer; - state.mut().epochRevenue = oldState.epochRevenue; - state.mut().epochQXMRRevenue = oldState.epochQXMRRevenue; - state.mut().qREAmount = oldState.qREAmount; - state.mut().totalBurnAmount = oldState.totalBurnAmount; - state.mut().totalCharityAmount = oldState.totalCharityAmount; - state.mut().totalShareholderAmount = oldState.totalShareholderAmount; - state.mut().totalRegisterAmount = oldState.totalRegisterAmount; - state.mut().totalFeeAmount = oldState.totalFeeAmount; - state.mut().totalWinnerAmount = oldState.totalWinnerAmount; - state.mut().largestWinnerAmount = oldState.largestWinnerAmount; - state.mut().numberOfRegisters = oldState.numberOfRegisters; - state.mut().numberOfQuRaffleMembers = oldState.numberOfQuRaffleMembers; - state.mut().numberOfEntryAmountSubmitted = oldState.numberOfEntryAmountSubmitted; - state.mut().numberOfProposals = oldState.numberOfProposals; - state.mut().numberOfActiveTokenRaffle = oldState.numberOfActiveTokenRaffle; - state.mut().numberOfEndedTokenRaffle = oldState.numberOfEndedTokenRaffle; - } - - PRE_ACQUIRE_SHARES() - { - // Accept all incoming share management transfers for free. - // Service fees are collected in user procedures (depositInTokenRaffle, TransferShareManagementRights). - output.requestedFee = 0; - output.allowTransfer = true; - } - - POST_ACQUIRE_SHARES() - { - // Credit any received fee to epochRevenue. - if (input.receivedFee > 0) - { - state.mut().epochRevenue += (uint64)input.receivedFee; - } - } -}; diff --git a/src/contracts/Qswap_old.h b/src/contracts/Qswap_old.h deleted file mode 100644 index ecf63457e..000000000 --- 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; - } -}; diff --git a/src/mining/ant_colony/ant_colony.h b/src/mining/ant_colony/ant_colony.h index b900f6a61..db935e476 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 40468c8c4..039a98e62 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 a95cb0482..a512d5ad3 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 98b123eb8..10c31ed2f 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 d4920889d..66d53cd88 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" @@ -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/qpi/impl/qpi_system_impl.h b/src/qpi/impl/qpi_system_impl.h index 383dacad9..bb98845dc 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 07854a4d9..032e79afa 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/src/qubic.cpp b/src/qubic.cpp index 27929295a..621fae505 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -2,7 +2,6 @@ // #define INCLUDE_CONTRACT_TEST_EXAMPLES -// #define OLD_QRAFFLE // contract_def.h needs to be included first to make sure that contracts have minimal access #include "contract_core/contract_def.h" @@ -609,12 +608,14 @@ static void antColonyBeginEpoch() // Every identity's root derives from this one value, so a node that seeded differently builds a // different forest and diverges. Logged as an identity so operators can compare it across nodes // by eye at epoch start, which is cheaper than finding out from a digest split later. +#ifndef NDEBUG CHAR16 digestChars[60 + 1]; getIdentity(gAntColony.rootSeed().m256i_u8, digestChars, true); CHAR16 msg[128]; setText(msg, L"[ant-colony] Root seed = "); appendText(msg, digestChars); - logToConsole(msg); + addDebugMessage(msg); +#endif } // DOGE merged-mining shares @@ -3439,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 31f0772c1..5798e8a4e 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/contract_nostromo.cpp b/test/contract_nostromo.cpp index 6f9c8e67d..a16d2233b 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); } diff --git a/test/contract_qip.cpp b/test/contract_qip.cpp index 90b0a9407..5cb8c0c89 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(); diff --git a/test/data/bpp9000.task b/test/data/bpp9000.task index 8ffa5e17a..38ce23517 100644 Binary files a/test/data/bpp9000.task and b/test/data/bpp9000.task differ diff --git a/test/data/gt_ant_production.csv b/test/data/gt_ant_production.csv index 664c75749..48aa11ed1 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 80569d037..5608faa4b 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 2fe37b43c..aa8cdace7 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";