diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 6e292e28..482d6a33 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -584,7 +584,7 @@ struct ContractStateChangeInfo // When enabling, replace both lines below, e.g.: //constexpr ContractStateChangeInfo contractStateChangeInfos[] = { { DUMMY_CONTRACT_INDEX, MIGRATE, 219 } }; //constexpr unsigned int contractStateChangeCount = sizeof(contractStateChangeInfos) / sizeof(contractStateChangeInfos[0]); -constexpr ContractStateChangeInfo contractStateChangeInfos[] = { { QIP_CONTRACT_INDEX, RESET, 224 }, { RANDOM_CONTRACT_INDEX, PADDING, 224 } }; +constexpr ContractStateChangeInfo contractStateChangeInfos[] = { { QIP_CONTRACT_INDEX, RESET, 224 }, { RANDOM_CONTRACT_INDEX, PADDING, 224 }, {NOST_CONTRACT_INDEX, MIGRATE, 229}}; constexpr unsigned int contractStateChangeCount = sizeof(contractStateChangeInfos) / sizeof(contractStateChangeInfos[0]); diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 1e50441e..e3e34c84 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -1,29 +1,146 @@ using namespace QPI; -constexpr uint64 NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT = 20000000ULL; -constexpr uint64 NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT = 100000000ULL; -constexpr uint64 NOSTROMO_TIER_DOG_STAKE_AMOUNT = 200000000ULL; -constexpr uint64 NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT = 800000000ULL; -constexpr uint64 NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT = 3200000000ULL; -constexpr uint64 NOSTROMO_QX_TOKEN_ISSUANCE_FEE = 1000000000ULL; - -constexpr uint32 NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT = 55; -constexpr uint32 NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT = 300; -constexpr uint32 NOSTROMO_TIER_DOG_POOL_WEIGHT = 750; -constexpr uint32 NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT = 3050; -constexpr uint32 NOSTROMO_TIER_WARRIOR_POOL_WEIGHT = 13750; - -constexpr uint32 NOSTROMO_TIER_FACEHUGGER_UNSTAKE_FEE = 5; -constexpr uint32 NOSTROMO_TIER_CHESTBURST_UNSTAKE_FEE = 4; -constexpr uint32 NOSTROMO_TIER_DOG_UNSTAKE_FEE = 3; -constexpr uint32 NOSTROMO_TIER_XENOMORPH_UNSTAKE_FEE = 2; -constexpr uint32 NOSTROMO_TIER_WARRIOR_UNSTAKE_FEE = 1; -constexpr uint32 NOSTROMO_CREATE_PROJECT_FEE = 100000000; - -constexpr uint32 NOSTROMO_MAX_USER = 262144; -constexpr uint32 NOSTROMO_MAX_NUMBER_PROJECT = 262144; -constexpr uint32 NOSTROMO_MAX_NUMBER_TOKEN = 262144; -constexpr uint32 NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST = 128; +namespace QPI +{ + inline bool operator==(const Asset& lhs, const Asset& rhs) + { + return lhs.assetName == rhs.assetName && lhs.issuer == rhs.issuer; + } +} // namespace QPI + +// Maximum number of active auction records stored by the contract, in auctions. +constexpr uint64 NOST_AUCTION_NUM = 2048; +// Number of full closed-auction snapshots retained in the history ring buffer, in entries. +constexpr uint64 NOST_AUCTION_HISTORY_NUM = 1024; +// Fixed length of an auction metadata IPFS CID, in bytes. +constexpr uint64 NOST_AUCTION_METADATA_CID_LENGTH = 64; +// Maximum number of active auction-participant bid records, in entries. +constexpr uint64 NOST_AUCTION_PARTICIPANT_NUM = 4096; +// Maximum number of wallets with unpaid QU obligations retained by the contract. +constexpr uint64 NOST_PENDING_PAYOUT_NUM = 8192; +// Maximum pending-payout slots one auction settlement may require before END_EPOCH fee distribution. +constexpr uint64 NOST_AUCTION_REVENUE_MAX_PAYOUT_RECIPIENTS = 1; +// Additional pending-payout slot reserved for the Batch bid caller's possible overpayment refund. +constexpr uint64 NOST_BATCH_BID_CALLER_PAYOUT_RECIPIENTS = 1; +// Maximum pending-payout slots reserved by a Standard bid for refunds and an immediate Buy Now settlement. +constexpr uint64 NOST_STANDARD_BID_MAX_PAYOUT_RECIPIENTS = 6; +// Maximum pending-payout slots reserved by Standard settlement for revenue distribution and bidder handling. +constexpr uint64 NOST_STANDARD_FINALIZATION_MAX_PAYOUT_RECIPIENTS = 5; +// Number of QPI-sized QU transfer chunks attempted for an immediate refund or settlement payout. +constexpr uint64 NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL = 1; +// Maximum number of pending-payout wallets retried automatically during one END_EPOCH call. +constexpr uint64 NOST_END_EPOCH_PAYOUT_RECIPIENT_NUM = 64; +// Number of QPI-sized QU transfer chunks retried per pending-payout wallet at END_EPOCH. +constexpr uint64 NOST_END_EPOCH_PAYOUT_CHUNKS_PER_RECIPIENT = 1; +// Maximum number of QPI-sized QU transfers attempted for one wallet in one procedure call. +constexpr uint64 NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL = 16; +// Sentinel for "no participant slot". +constexpr uint64 NOST_INVALID_PARTICIPANT_SLOT = NOST_AUCTION_PARTICIPANT_NUM; +// Maximum number of entries returned by one paginated auction getter call. +constexpr uint64 NOST_AUCTION_GETTER_PAGE_SIZE = 64; +// Maximum number of asset entries in a Batch Auction lot. +constexpr uint64 NOST_BATCH_AUCTION_LOT_ITEM_NUM = 1; +// Integer offset that makes the Batch coverage threshold include the first quantity below the minimum allocation. +constexpr uint64 NOST_BATCH_COVERAGE_THRESHOLD_OFFSET = 1; +// Maximum number of asset entries in a Standard Auction lot. +constexpr uint64 NOST_AUCTION_LOT_ITEM_NUM = 4; +// Maximum number of bidder wallets allowed by a private auction wallet gate. +constexpr uint64 NOST_AUCTION_ALLOWED_WALLET_NUM = 16; +// Maximum number of alternative assets accepted by a private auction asset gate. +constexpr uint64 NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM = 4; +// Maximum configured duration of any auction, in days. +constexpr uint32 NOST_AUCTION_MAX_DURATION_DAYS = 30; +// Default fee charged to create a private auction, in qu. +constexpr sint64 NOST_DEFAULT_PRIVATE_AUCTION_FEE = 50000000LL; +// Default fee accumulated after successfully creating a public auction and distributed at END_EPOCH, in qu. +constexpr sint64 NOST_PUBLIC_AUCTION_CREATION_FEE = 100LL; +// Minimum total payment target for small accepted Batch Auction bids, in qu. +constexpr uint64 NOST_BATCH_BID_FEE_CUTOFF = 100ULL; +// Default fee deducted when an auction is cancelled, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_CANCELLATION_FEE_BP = 1000ULL; +// Default management fee applied to gross auction proceeds, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_MANAGEMENT_FEE_BP = 50ULL; +// Default development fee applied to gross auction proceeds, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_DEVELOPMENT_FEE_BP = 50ULL; +// Default takeover coordinator fee applied to gross auction proceeds, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_TAKEOVER_COORDINATOR_FEE_BP = 50ULL; +// Shareholder allocation of auction creation, small-bid, and cancellation service fees, in basis points. +constexpr uint64 NOST_AUCTION_SERVICE_FEE_SHAREHOLDER_BP = 7270ULL; +// Management allocation of auction creation, small-bid, and cancellation service fees, in basis points. +constexpr uint64 NOST_AUCTION_SERVICE_FEE_MANAGEMENT_BP = 910ULL; +// Development allocation of auction creation, small-bid, and cancellation service fees, in basis points. +constexpr uint64 NOST_AUCTION_SERVICE_FEE_DEVELOPMENT_BP = 910ULL; +// Takeover coordinator allocation of auction creation, small-bid, and cancellation service fees, in basis points. +constexpr uint64 NOST_AUCTION_SERVICE_FEE_TAKEOVER_COORDINATOR_BP = 910ULL; +// Default portion of the shareholder fee distributed as dividends, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_DIVIDEND_BP = 9000ULL; +// Default shareholder fee for gross proceeds in tier 1, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_1 = 500ULL; +// Default shareholder fee for gross proceeds in tier 2, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_2 = 450ULL; +// Default shareholder fee for gross proceeds in tier 3, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_3 = 400ULL; +// Default shareholder fee for gross proceeds in tier 4, in basis points. +constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4 = 350ULL; +// Inclusive upper gross-proceeds threshold for shareholder fee tier 1, in qu. +constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1 = 5000000000ULL; +// Inclusive upper gross-proceeds threshold for shareholder fee tier 2, in qu. +constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2 = 50000000000ULL; +// Inclusive upper gross-proceeds threshold for shareholder fee tier 3, in qu. +constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3 = 200000000000ULL; +// Time added when an accepted bid arrives near an auction deadline, in seconds. +constexpr uint64 NOST_AUCTION_EXTENSION_SECONDS = 300ULL; +// Number of seconds used to convert one auction duration day. +constexpr uint64 NOST_SECONDS_PER_DAY = 86400ULL; +// Time allowed for a Standard Auction seller to resolve a pending sale, in seconds. +constexpr uint64 NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS = 604800ULL; +// Duration of the scheduled auction pause before an epoch transition, in seconds. +constexpr uint64 NOST_AUCTION_PRE_EPOCH_PAUSE_SECONDS = 1800ULL; +// Duration of the auction launch pause after `BEGIN_EPOCH`, in ticks. +constexpr uint32 NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS = 500U; +// Denominator representing 100 percent in basis-point calculations. +constexpr uint64 NOST_BASIS_POINTS_SCALE = 10000ULL; +// Number of microseconds used to convert a timestamp duration to seconds. +constexpr uint64 NOST_MICROSECONDS_PER_SECOND = 1000000ULL; +// Epoch at which the contract reapplies its default configuration, in epochs. +constexpr uint16 NOST_REINITIALIZATION_EPOCH = 220U; +// Quantity used to sell a Standard Auction lot as one indivisible unit, not an asset count. +constexpr uint64 NOST_STANDARD_AUCTION_LOT_COUNT = 1ULL; +// Minimum allowed Standard Auction starting and sale price, in qu. +constexpr uint64 NOST_STANDARD_MIN_PRICE = 1000000ULL; +// Minimum allowed Standard Auction bid increment, in qu. +constexpr uint64 NOST_STANDARD_MIN_BID_INCREMENT = 1000ULL; +// Year component of the packed initial date stamp. +constexpr uint8 NOST_DEFAULT_INIT_YEAR = 22U; +// Month component of the packed initial date stamp. +constexpr uint8 NOST_DEFAULT_INIT_MONTH = 4U; +// Day component of the packed initial date stamp. +constexpr uint8 NOST_DEFAULT_INIT_DAY = 13U; +// Bit offset of the year component in a packed date stamp, in bits. +constexpr uint8 NOST_DATE_STAMP_YEAR_SHIFT = 9U; +// Bit offset of the month component in a packed date stamp, in bits. +constexpr uint8 NOST_DATE_STAMP_MONTH_SHIFT = 5U; +// Runtime day-of-week index on which the scheduled pre-epoch pause begins. +constexpr uint8 NOST_PRE_EPOCH_PAUSE_DAY_OF_WEEK = 0U; +// UTC hour at which the scheduled pre-epoch pause begins. +constexpr uint8 NOST_PRE_EPOCH_PAUSE_HOUR = 11U; +// Minute within the configured hour at which the scheduled pre-epoch pause begins. +constexpr uint8 NOST_PRE_EPOCH_PAUSE_MINUTE = 30U; +// Packed date stamp used to recognize the contract's initial runtime date. +constexpr uint32 NOST_DEFAULT_INIT_TIME = + NOST_DEFAULT_INIT_YEAR << NOST_DATE_STAMP_YEAR_SHIFT | NOST_DEFAULT_INIT_MONTH << NOST_DATE_STAMP_MONTH_SHIFT | NOST_DEFAULT_INIT_DAY; +// Default enabled flag that routes all collected auction fees to development. +constexpr uint8 NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT = 1; +// Default drop in the execution fee reserve that triggers an emergency pause, in basis points. +constexpr uint64 NOST_DEFAULT_FEE_RESERVE_GUARD_DROP_BP = 1000ULL; +// Default rolling window used to evaluate the execution fee reserve drop, in seconds. +constexpr uint64 NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS = 600ULL; + +/** Old */ +constexpr uint32 NOSTROMO_MAX_USER_OLD = 262144; +constexpr uint32 NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST_OLD = 128; +constexpr uint32 NOSTROMO_MAX_NUMBER_TOKEN_OLD = 262144; +constexpr uint32 NOSTROMO_MAX_NUMBER_PROJECT_OLD = 262144; struct NOST2 { @@ -31,185 +148,324 @@ struct NOST2 struct NOST : public ContractBase { -public: - /****** PORTED TIMEUTILS FROM OLD Nostromo *****/ - /** - * Compare 2 date in uint32 format - * @return -1 lesser(ealier) AB - */ - inline static sint32 dateCompare(uint32& A, uint32& B, sint32& i) + enum class EProcedureId : uint8 { - if (A == B) return 0; - if (A < B) return -1; - return 1; - } + CreateAuction = 1, + PlaceBid = 2, + CancelAuction = 3, + TransferShareManagementRights = 4, + ResolvePendingStandardAuction = 5, + SetAuctionFees = 6, + SetAuctionFeesByManagement = 7, + SetManagement = 8, + SetFeeReserveGuardConfig = 9, + SetEmergencyPause = 10 + }; - /** - * @return pack Nost datetime data from year, month, day, hour, minute, second to a uint32 - * year is counted from 24 (2024) - */ - inline static void packNostromoDate(uint32 _year, uint32 _month, uint32 _day, uint32 _hour, uint32 _minute, uint32 _second, uint32& res) + /** @brief Stable public-function identifiers used by the contract ABI. */ + enum class EFunctionId : uint16 { - res = ((_year - 24) << 26) | (_month << 22) | (_day << 17) | (_hour << 12) | (_minute << 6) | (_second); - } + GetAuctionByIndex = 1, + GetAuctionParticipant = 2, + GetTicksBeforeAuctionLaunch = 3, + GetAuctionFees = 4, + GetFeeRecipients = 5, + GetClosedAuctionHistory = 6, + GetRouteAllFeesToDevelopment = 7, + GetContractStats = 8, + GetAuctionSummaries = 9, + GetActiveAuctionIndices = 10, + GetAuctionsBySeller = 11, + GetAuctionByMetadataCid = 12, + GetAuctionSummariesByIndexBatch = 13, + GetAuctionParticipants = 14, + GetUserParticipations = 15, + GetLatestAuctionIndex = 16, + GetAuctionCountBySeller = 17, + GetAuctionAtCreationSnapshot = 18, + GetBatchAuctionBidAvailability = 19, + CalculateBatchAuctionBidFee = 20, + GetPendingServiceFeePool = 21, + GetFeeReserveGuardState = 22, + GetPendingPayout = 23, + GetNostromoFeePool = 24 + }; - inline static uint32 NostGetYear(uint32 data) - { - return ((data >> 26) + 24); - } - inline static uint32 NostGetMonth(uint32 data) - { - return ((data >> 22) & 0b1111); - } - inline static uint32 NostGetDay(uint32 data) - { - return ((data >> 17) & 0b11111); - } - inline static uint32 NostGetHour(uint32 data) - { - return ((data >> 12) & 0b11111); - } - inline static uint32 NostGetMinute(uint32 data) + enum class EAuctionType : uint8 { - return ((data >> 6) & 0b111111); - } - inline static uint32 NostGetSecond(uint32 data) + None, + Batch, + Standard + }; + + enum class EAuctionVisibility : uint8 { - return (data & 0b111111); - } - /* - * @return unpack Nost datetime from uin32 to year, month, day, hour, minute, secon - */ - inline static void unpackNostromoDate(uint8& _year, uint8& _month, uint8& _day, uint8& _hour, uint8& _minute, uint8& _second, uint32 data) + None, + Public, + Private + }; + + enum class EAuctionStatus : uint8 { - _year = NostGetYear(data); // 6 bits - _month = NostGetMonth(data); //4bits - _day = NostGetDay(data); //5bits - _hour = NostGetHour(data); //5bits - _minute = NostGetMinute(data); //6bits - _second = NostGetSecond(data); //6bits - } + None, + Active, + Finalized, + Cancelled, + PendingSellerDecision + }; - inline static void accumulatedDay(sint32 month, uint64& res) + enum class EAuctionError : uint8 { - switch (month) - { - case 1: res = 0; break; - case 2: res = 31; break; - case 3: res = 59; break; - case 4: res = 90; break; - case 5: res = 120; break; - case 6: res = 151; break; - case 7: res = 181; break; - case 8: res = 212; break; - case 9: res = 243; break; - case 10:res = 273; break; - case 11:res = 304; break; - case 12:res = 334; break; - } - } + Success, + InvalidInput, + AuctionNotFound, + AuctionClosed, + Forbidden, + InsufficientFunds, + InsufficientAssetBalance, + StorageFull, + InvalidAuctionType, + InvalidVisibility, + BidTooLow, + PrivateAuctionAccessDenied, + AuctionPaused, + AuctionIndexExhausted, + QuantityUnavailable, + AuctionHasAcceptedBid, + PayoutQueueFull + }; + /** - * @return difference in number of second, A must be smaller than or equal B to have valid value + * @brief Stores one bid slot in one auction. + * @note The same struct is shared by batch and standard auctions. */ - inline static void diffDateInSecond(uint32& A, uint32& B, sint32& i, uint64& dayA, uint64& dayB, uint64& res) + struct AuctionParticipantData { - if (dateCompare(A, B, i) >= 0) - { - res = 0; - return; - } - accumulatedDay(NostGetMonth(A), dayA); - dayA += NostGetDay(A); - accumulatedDay(NostGetMonth(B), dayB); - dayB += (NostGetYear(B) - NostGetYear(A)) * 365ULL + NostGetDay(B); + /** @brief Auction that owns this bid slot. */ + uint64 auctionIndex; - // handling leap-year: only store last 2 digits of year here, don't care about mod 100 & mod 400 case - for (i = NostGetYear(A); (uint32)(i) < NostGetYear(B); i++) - { - if (mod(i, 4) == 0) - { - dayB++; - } - } - if (mod(sint32(NostGetYear(A)), 4) == 0 && (NostGetMonth(A) > 2)) dayA++; - if (mod(sint32(NostGetYear(B)), 4) == 0 && (NostGetMonth(B) > 2)) dayB++; - res = (dayB - dayA) * 3600ULL * 24; - res += (NostGetHour(B) * 3600 + NostGetMinute(B) * 60 + NostGetSecond(B)); - res -= (NostGetHour(A) * 3600 + NostGetMinute(A) * 60 + NostGetSecond(A)); - } + /** @brief Monotonic bid sequence inside the auction, used for FIFO tie-breaks. */ + uint64 bidIndex; - inline static bool checkValidNostDateTime(uint32& A) - { - if (NostGetMonth(A) > 12) return false; - if (NostGetDay(A) > 31) return false; - if ((NostGetDay(A) == 31) && - (NostGetMonth(A) != 1) && (NostGetMonth(A) != 3) && (NostGetMonth(A) != 5) && - (NostGetMonth(A) != 7) && (NostGetMonth(A) != 8) && (NostGetMonth(A) != 10) && (NostGetMonth(A) != 12)) return false; - if ((NostGetDay(A) == 30) && (NostGetMonth(A) == 2)) return false; - if ((NostGetDay(A) == 29) && (NostGetMonth(A) == 2) && (mod(NostGetYear(A), 4u) != 0)) return false; - if (NostGetHour(A) >= 24) return false; - if (NostGetMinute(A) >= 60) return false; - if (NostGetSecond(A) >= 60) return false; - return true; - } + /** @brief Amount currently locked in escrow for the participant bid. */ + uint64 escrowedAmount; + + /** @brief Quantity requested by the participant; standard auctions always use the whole lot quantity. */ + uint64 requestedQuantity; + + /** @brief Quantity finally allocated to the participant after batch auction settlement. */ + uint64 allocatedQuantity; + + /** @brief Offered price per asset in a batch auction, or total offered price for the whole lot in a standard auction. */ + uint64 bidAmount; + + /** @brief Wallet that owns this participant record. */ + id participant; - /****** END PORTED TIMEUTILS FROM OLD Nostromo *****/ + /** @brief Timestamp of the participant's latest accepted bid. */ + DateAndTime lastBidTime; - struct investInfo + /** @brief Marks whether this fixed array slot contains a reusable historical or active record. */ + uint8 isUsed; + + /** @brief Marks bids that are still eligible for allocation or standard highest-bid settlement. */ + uint8 isActive; + + /** @brief Marks bids that remain inside the winning allocation after settlement. */ + uint8 isWinningBid; + }; + + /** + * @brief Describes an asset and quantity used by an auction lot or private access rule. + */ + struct AuctionAssetEntry { - uint64 investedAmount; - uint64 claimedAmount; - uint32 indexOfFundraising; + /** @brief Asset included in a lot or used as an access requirement. */ + Asset asset; + + /** @brief Lot quantity or minimum ownership quantity required for access. */ + sint64 quantity; }; - struct projectInfo + /** + * @brief Shared auction fields used by persistent state and public getter views. + * @note Container fields differ between persistent state and ABI views, so access-control collections stay outside this struct. + * @note `metadataIpfsCid` points to off-chain auction metadata stored in IPFS. + * @note `sellerDecisionDeadline` stays zero until a standard auction enters the manual decision window. + */ + struct AuctionCore { - id creator; - uint64 tokenName; - uint64 supplyOfToken; - uint32 startDate; - uint32 endDate; - uint32 numberOfYes; - uint32 numberOfNo; - bit isCreatedFundarasing; + /** @brief Assets and quantities offered by the auction. */ + Array auctionLotItems; + + /** @brief Lowercase base32 CIDv1 stored in Pinata for the auction name and description metadata. */ + Array metadataIpfsCid; + + /** @brief Wallet that created the auction and offers the lot for sale. */ + id seller; + + /** @brief Wallet that currently holds the highest bid. */ + id highestBidder; + + /** @brief Timestamp when the seller created the auction. */ + DateAndTime createdAt; + + /** @brief Timestamp of the most recent accepted bid. */ + DateAndTime lastBidAt; + + /** @brief Deadline for the seller to accept or reject a standard auction bid that ended between Initial Price and Sale Price. */ + DateAndTime sellerDecisionDeadline; + + /** @brief Timestamp when the auction was finalized, cancelled, or otherwise settled. */ + DateAndTime settledAt; + + /** @brief Total sale units offered; batch auctions use asset quantity, standard auctions use one unit for the whole lot. */ + uint64 quantityForSale; + + /** @brief Quantity already assigned to winning bids after settlement. */ + uint64 allocatedQuantity; + + /** @brief Minimum quantity requested by each batch bid; always zero for standard auctions. */ + uint64 minimumPurchaseQuantity; + + /** @brief Initial price for a standard auction; bids cannot start below this total price for the whole lot. */ + uint64 initialPrice; + + /** @brief Minimum selling price per asset in a batch auction, or desired minimum total selling price for the whole lot in a standard + * auction. */ + uint64 salePrice; + + /** @brief Minimum increment by which a new standard auction bid must exceed the current highest bid. */ + uint64 minimumBidIncrement; + + /** @brief Buy Now price that closes a standard auction immediately when matched or exceeded. */ + uint64 buyNowPrice; + + /** @brief Highest offered price per asset in a batch auction, or highest total offered price in a standard auction. */ + uint64 highestBidPrice; + + /** @brief Quantity requested by the current highest bid. */ + uint64 highestBidQuantity; + + /** @brief Total amount escrowed by the current highest bid; equal to the committed highest bid amount. */ + uint64 highestBidAmount; + + /** @brief Auction duration in seconds, derived from the duration configured in days. */ + uint64 auctionDurationSeconds; + + /** @brief Monotonic identifier assigned when the auction is created. */ + uint64 auctionIndex; + + /** @brief Monotonic per-auction bid index used to store every batch bid as a separate position. */ + uint64 nextBidIndex; + + /** @brief Fixed-array slot of the current standard-auction highest bid, or `NOST_INVALID_PARTICIPANT_SLOT`. */ + uint64 highestBidSlotIndex; + + /** @brief Auction House mode: Batch Auction or Standard Auction. */ + EAuctionType type; + + /** @brief Auction visibility: public or restricted private access. */ + EAuctionVisibility visibility; + + /** @brief Current lifecycle status of the auction, including the seller decision phase for standard auctions. */ + EAuctionStatus status; }; - struct fundaraisingInfo + /** + * @brief Stores all persistent data for one auction. + * @note The same struct is shared by batch and standard auctions. + */ + struct AuctionData { - uint64 tokenPrice; - uint64 soldAmount; - uint64 requiredFunds; - uint64 raisedFunds; - uint32 indexOfProject; - uint32 firstPhaseStartDate; - uint32 firstPhaseEndDate; - uint32 secondPhaseStartDate; - uint32 secondPhaseEndDate; - uint32 thirdPhaseStartDate; - uint32 thirdPhaseEndDate; - uint32 listingStartDate; - uint32 cliffEndDate; - uint32 vestingEndDate; - uint8 threshold; - uint8 TGE; - uint8 stepOfVesting; - bit isCreatedToken; + /** @brief Fields shared with the public auction view. */ + AuctionCore core; + + /** @brief Wallet whitelist used when the private auction uses wallet-based access. */ + HashSet allowedBidderWallets; + + /** @brief Minimum quantity by asset required for participation; owning any one entry grants access. */ + HashMap requiredAccessAssets; }; - struct StateData + /** + * @brief Serializable view of one auction for public getter outputs. + * @note Persistent state uses `HashSet` for access checks, but ABI payloads expose fixed arrays because `HashSet` is not valid in + * input/output structs. + */ + struct AuctionView { - HashMap users; - HashMap, NOSTROMO_MAX_USER> voteStatus; - HashMap numberOfVotedProject; - HashSet tokens; + /** @brief Fields shared with the persistent auction record. */ + AuctionCore core; + + /** @brief Wallet list used when the private auction restricts participation to predefined wallets. */ + Array allowedBidderWallets; + + /** @brief Asset and minimum-quantity alternatives used by private asset-based access. */ + Array requiredAccessAssets; + + /** @brief Number of populated entries in `requiredAccessAssets`. */ + uint64 requiredAccessAssetCount; - HashMap, NOSTROMO_MAX_USER> investors; - HashMap numberOfInvestedProjects; - Array tmpInvestedList; + /** @brief Number of populated entries in `allowedBidderWallets`. */ + uint64 allowedBidderWalletCount; + }; - Array projects; + struct OldStateData + { + struct investInfo + { + uint64 investedAmount; + uint64 claimedAmount; + uint32 indexOfFundraising; + }; - Array fundaraisings; + struct projectInfo + { + id creator; + uint64 tokenName; + uint64 supplyOfToken; + uint32 startDate; + uint32 endDate; + uint32 numberOfYes; + uint32 numberOfNo; + bit isCreatedFundarasing; + }; + + struct fundaraisingInfo + { + uint64 tokenPrice; + uint64 soldAmount; + uint64 requiredFunds; + uint64 raisedFunds; + uint32 indexOfProject; + uint32 firstPhaseStartDate; + uint32 firstPhaseEndDate; + uint32 secondPhaseStartDate; + uint32 secondPhaseEndDate; + uint32 thirdPhaseStartDate; + uint32 thirdPhaseEndDate; + uint32 listingStartDate; + uint32 cliffEndDate; + uint32 vestingEndDate; + uint8 threshold; + uint8 TGE; + uint8 stepOfVesting; + bit isCreatedToken; + }; + + HashMap users; + HashMap, NOSTROMO_MAX_USER_OLD> voteStatus; + HashMap numberOfVotedProject; + HashSet tokens; + + HashMap, NOSTROMO_MAX_USER_OLD> investors; + HashMap numberOfInvestedProjects; + Array tmpInvestedList; + + Array projects; + + Array fundaraisings; id teamAddress; sint64 transferRightsFee; @@ -217,1432 +473,6044 @@ struct NOST : public ContractBase uint32 numberOfRegister, numberOfCreatedProject, numberOfFundraising; }; - struct registerInTier_input + /** + * @brief Epoch fee accrual shared by Nostromo modules. + * @note Auction shareholder amounts are separated by sale tier because their fee formulas differ. Other recipient amounts are compatible sums. + */ + struct NostromoFeePool + { + uint64 shareholderDividendTier1Amount; + uint64 shareholderDividendTier2Amount; + uint64 shareholderDividendTier3Amount; + uint64 shareholderDividendTier4Amount; + uint64 commonServiceFeeAmount; + uint64 shareholderDividendAmount; + uint64 managementAmount; + uint64 developmentAmount; + uint64 takeoverCoordinatorAmount; + }; + + struct StateData + { + /** @brief Configured fee charged when creating a private auction. */ + sint64 privateAuctionFee; + + /** @brief Configured non-negative fee accumulated when creating a public auction and distributed at `END_EPOCH`. */ + sint64 publicAuctionCreationFee; + + /** @brief Configured cancellation fee rate in basis points. */ + uint64 auctionCancellationFeeBasisPoints; + + /** @brief Undistributed shareholder revenue from the shared fee pool reserved for contract dividends. */ + uint64 auctionShareholderDividendPool; + + /** @brief Configured management fee rate in basis points, charged from auction proceeds. */ + uint64 managementFeeBasisPoints; + + /** @brief Configured development fee rate in basis points, charged from auction proceeds. */ + uint64 developmentFeeBasisPoints; + + /** @brief Configured takeover coordinator fee rate in basis points, charged from auction proceeds. */ + uint64 takeoverCoordinatorFeeBasisPoints; + + /** @brief Share of the shareholder fee redirected to dividends, expressed in basis points. */ + uint64 shareholderDividendBasisPoints; + + /** @brief Shareholder fee tier applied to auctions up to the first threshold. */ + uint64 shareholderFeeBasisPointsTier1; + + /** @brief Shareholder fee tier applied to auctions above the first threshold and up to the second threshold. */ + uint64 shareholderFeeBasisPointsTier2; + + /** @brief Shareholder fee tier applied to auctions above the second threshold and up to the third threshold. */ + uint64 shareholderFeeBasisPointsTier3; + + /** @brief Shareholder fee tier applied to auctions above the third threshold. */ + uint64 shareholderFeeBasisPointsTier4; + + /** @brief Start of the currently active global auction timer pause interval. */ + DateAndTime auctionTimerPauseStartedAt; + + /** @brief End of the currently active global auction timer pause interval. */ + DateAndTime auctionTimerPauseEndsAt; + + /** @brief Configured maximum auction duration in days. */ + uint32 maxAuctionDurationDays; + + /** @brief Cached QX transfer fee refreshed at the beginning of each epoch. */ + uint32 qxTransferFee; + + /** @brief Flag indicating whether the post-`BEGIN_EPOCH()` auction pause is active for the current epoch. */ + uint8 isPostBeginEpochPauseArmed; + + /** @brief Flag indicating whether auction deadlines are currently frozen by a global pause interval. */ + uint8 isAuctionTimerPaused; + + /** @brief Flag indicating whether every auction fee is routed to the development wallet. */ + uint8 routeAllFeesToDevelopment; + + id management; + + id development; + + id takeoverCoordinator; + + /** @brief Total number of auctions ever created; also the next auction index. */ + uint64 totalAuctionsCreated; + + /** @brief Circular buffer with full snapshots of finalized and cancelled auctions. */ + Array closedAuctionHistory; + + /** @brief Monotonic insertion counter for `closedAuctionHistory`. */ + uint64 closedAuctionHistoryCounter; + + HashMap auctionList; + /** @brief Active bid records; slots are cleared as soon as the bid leaves the live order book. */ + Array participants; + /** @brief Bounded history of completed, refunded, and displaced bid records. */ + Array participantHistory; + /** @brief Monotonic insertion counter for `participantHistory`. */ + uint64 participantHistoryCounter; + + /** @brief Wallet-indexed QU liabilities registered before an auction is finalized. */ + HashMap pendingQuPayouts; + /** @brief Sum of all values in `pendingQuPayouts`, in qu. */ + uint64 totalPendingQuPayouts; + /** @brief Physical hash-map slot from which the next bounded automatic payout scan starts. */ + uint64 pendingPayoutScanCursor; + /** @brief Lifetime number of finalized auctions. */ + uint64 totalFinalizedAuctions; + /** @brief Lifetime number of cancelled auctions. */ + uint64 totalCancelledAuctions; + + /** @brief Shared fee accrual for Auction House and future Nostromo modules, settled at `END_EPOCH`. */ + NostromoFeePool feePool; + + /** @brief Configured drop in the execution fee reserve that triggers an emergency pause, in basis points. */ + uint64 feeReserveGuardDropBasisPoints; + + /** @brief Configured rolling window used to evaluate the execution fee reserve drop, in seconds. */ + uint64 feeReserveGuardWindowSeconds; + + /** @brief Execution fee reserve value recorded at the start of the current guard window. */ + sint64 feeReserveBaseline; + + /** @brief Start of the current guard window; invalid when the window has not been initialized. */ + DateAndTime feeReserveBaselineAt; + + /** @brief Timestamp at which the emergency pause was triggered; invalid when not paused. */ + DateAndTime emergencyPausedAt; + + /** @brief Flag indicating whether an emergency pause is currently blocking auction interactions. */ + uint8 isEmergencyPaused; + }; + + /** @brief Input payload used to create a Batch Auction or Standard Auction in the Auction House. */ + struct CreateAuction_input { - uint32 tierLevel; + /** @brief Lowercase base32 CIDv1 stored in Pinata for the auction name and description metadata. */ + Array metadataIpfsCid; + + /** @brief Assets and quantities offered by the auction. */ + Array auctionLotItems; + + /** @brief Asset and minimum-quantity alternatives used by private asset-based access. */ + Array requiredAccessAssets; + + /** @brief Wallet list used when the private auction restricts participation to predefined wallets. */ + Array allowedBidderWallets; + + /** @brief Required minimum requested quantity for batch bids; ignored for standard auctions. */ + uint64 minimumPurchaseQuantity; + + /** @brief Initial price for a standard auction; bids cannot be placed below this total price for the whole lot. */ + uint64 initialPrice; + + /** @brief Minimum selling price per asset in a batch auction, or desired minimum total selling price for the whole lot in a standard + * auction. */ + uint64 salePrice; + + /** @brief Minimum increment by which each new standard auction bid must exceed the current highest bid. */ + uint64 minimumBidIncrement; + + /** @brief Buy Now price that immediately closes a standard auction once matched or exceeded. */ + uint64 buyNowPrice; + + /** @brief Auction duration in days, capped by the contract configuration. */ + uint32 durationDays; + + /** @brief Auction House mode selected by the seller: Batch Auction or Standard Auction. */ + uint8 auctionType; + + /** @brief Visibility selected by the seller: public or private. */ + uint8 auctionVisibility; }; - struct registerInTier_output + /** @brief Result of auction creation. */ + struct CreateAuction_output { - uint32 tierLevel; + /** @brief Monotonic index assigned to the new auction when creation succeeds. */ + uint64 auctionIndex; + + /** @brief Result code describing whether the auction creation succeeded. */ + EAuctionError errorCode; }; - struct logoutFromTier_input + /** @brief Input payload used to place a bid in a Batch Auction or Standard Auction. */ + struct PlaceBid_input { + /** @brief Monotonic index of the target auction. */ + uint64 auctionIndex; + + /** @brief Requested quantity for a batch auction, which must meet its configured minimum; ignored for a standard auction. */ + uint64 quantity; + /** @brief Offered price per asset in a batch auction, or total offered price for the whole lot in a standard auction. */ + uint64 bidAmount; }; - struct logoutFromTier_output + /** @brief Result of a bid placement request. */ + struct PlaceBid_output { - bit result; + /** @brief Amount that remains escrowed for the accepted bid. */ + uint64 escrowedAmount; + + /** @brief Amount refunded to the bidder, including replaced escrow or invocation change. */ + uint64 refundedAmount; + + /** @brief Result code describing whether the bid placement succeeded. */ + EAuctionError errorCode; }; - struct createProject_input + /** @brief Input payload used to cancel an active auction. */ + struct CancelAuction_input { - uint64 tokenName; - uint64 supply; - uint32 startYear; - uint32 startMonth; - uint32 startDay; - uint32 startHour; - uint32 endYear; - uint32 endMonth; - uint32 endDay; - uint32 endHour; + /** @brief Monotonic index of the auction that the seller wants to cancel. */ + uint64 auctionIndex; }; - struct createProject_output + /** @brief Result of an auction cancellation request. */ + struct CancelAuction_output { - uint32 indexOfProject; + /** @brief Total amount refunded to bidders because of the cancellation. */ + uint64 refundedAmount; + + /** @brief Cancellation fee charged to the seller according to the auction rules. */ + uint64 cancellationFee; + + /** @brief Result code describing whether the cancellation succeeded. */ + EAuctionError errorCode; }; - struct voteInProject_input + /** @brief Input payload used by the seller to accept or reject a pending standard auction result. */ + struct ResolvePendingStandardAuction_input { - uint32 indexOfProject; - bit decision; + /** @brief Monotonic index of the standard auction awaiting the seller decision. */ + uint64 auctionIndex; + + /** @brief Set to `1` to accept the sale or `0` to reject it. */ + uint8 acceptSale; }; - struct voteInProject_output + /** @brief Result of a seller decision on a pending standard auction. */ + struct ResolvePendingStandardAuction_output { + /** @brief Amount refunded to the bidder when the seller rejects the sale. */ + uint64 refundedAmount; + /** @brief Result code describing whether the seller decision was applied. */ + EAuctionError errorCode; }; - struct createFundraising_input + /** @brief Input payload used by the takeover coordinator to overwrite the full auction fee configuration. */ + struct SetAuctionFees_input { - uint64 tokenPrice; - uint64 soldAmount; - uint64 requiredFunds; + /** @brief Fee charged when a private auction is created. */ + sint64 privateAuctionFee; - uint32 indexOfProject; - uint32 firstPhaseStartYear; - uint32 firstPhaseStartMonth; - uint32 firstPhaseStartDay; - uint32 firstPhaseStartHour; - uint32 firstPhaseEndYear; - uint32 firstPhaseEndMonth; - uint32 firstPhaseEndDay; - uint32 firstPhaseEndHour; + /** @brief Non-negative fee accumulated when a public auction is created and distributed at `END_EPOCH`. */ + sint64 publicAuctionCreationFee; - uint32 secondPhaseStartYear; - uint32 secondPhaseStartMonth; - uint32 secondPhaseStartDay; - uint32 secondPhaseStartHour; - uint32 secondPhaseEndYear; - uint32 secondPhaseEndMonth; - uint32 secondPhaseEndDay; - uint32 secondPhaseEndHour; + /** @brief Cancellation fee rate in basis points. */ + uint64 auctionCancellationFeeBasisPoints; - uint32 thirdPhaseStartYear; - uint32 thirdPhaseStartMonth; - uint32 thirdPhaseStartDay; - uint32 thirdPhaseStartHour; - uint32 thirdPhaseEndYear; - uint32 thirdPhaseEndMonth; - uint32 thirdPhaseEndDay; - uint32 thirdPhaseEndHour; + /** @brief Management fee rate in basis points. */ + uint64 managementFeeBasisPoints; - uint32 listingStartYear; - uint32 listingStartMonth; - uint32 listingStartDay; - uint32 listingStartHour; + /** @brief Development fee rate in basis points. */ + uint64 developmentFeeBasisPoints; - uint32 cliffEndYear; - uint32 cliffEndMonth; - uint32 cliffEndDay; - uint32 cliffEndHour; + /** @brief Takeover coordinator fee rate in basis points. */ + uint64 takeoverCoordinatorFeeBasisPoints; - uint32 vestingEndYear; - uint32 vestingEndMonth; - uint32 vestingEndDay; - uint32 vestingEndHour; + /** @brief Percentage of the shareholder fee distributed as dividends, in basis points. */ + uint64 shareholderDividendBasisPoints; - uint8 threshold; - uint8 TGE; - uint8 stepOfVesting; - }; + /** @brief Shareholder fee tier for auctions up to the first threshold. */ + uint64 shareholderFeeBasisPointsTier1; - struct createFundraising_output - { + /** @brief Shareholder fee tier for auctions above the first threshold and up to the second threshold. */ + uint64 shareholderFeeBasisPointsTier2; + + /** @brief Shareholder fee tier for auctions above the second threshold and up to the third threshold. */ + uint64 shareholderFeeBasisPointsTier3; + /** @brief Shareholder fee tier for auctions above the third threshold. */ + uint64 shareholderFeeBasisPointsTier4; }; - struct investInProject_input + struct SetAuctionFees_output { - uint32 indexOfFundraising; + /** @brief Result code describing whether the fee update succeeded. */ + EAuctionError errorCode; }; - struct investInProject_output + /** @brief Input payload used by management to update every fee except takeover coordinator-specific splits. */ + struct SetAuctionFeesByManagement_input { + /** @brief Fee charged when a private auction is created. */ + sint64 privateAuctionFee; + /** @brief Non-negative fee accumulated when a public auction is created and distributed at `END_EPOCH`. */ + sint64 publicAuctionCreationFee; + + /** @brief Cancellation fee rate in basis points. */ + uint64 auctionCancellationFeeBasisPoints; + + /** @brief Management fee rate in basis points. */ + uint64 managementFeeBasisPoints; + + /** @brief Development fee rate in basis points. */ + uint64 developmentFeeBasisPoints; + + /** @brief Shareholder fee tier for auctions up to the first threshold. */ + uint64 shareholderFeeBasisPointsTier1; + + /** @brief Shareholder fee tier for auctions above the first threshold and up to the second threshold. */ + uint64 shareholderFeeBasisPointsTier2; + + /** @brief Shareholder fee tier for auctions above the second threshold and up to the third threshold. */ + uint64 shareholderFeeBasisPointsTier3; + + /** @brief Shareholder fee tier for auctions above the third threshold. */ + uint64 shareholderFeeBasisPointsTier4; }; - struct claimToken_input + struct SetAuctionFeesByManagement_output { - uint64 amount; - uint32 indexOfFundraising; + /** @brief Result code describing whether the fee update succeeded. */ + EAuctionError errorCode; }; - struct claimToken_output + /** @brief Input payload used by the takeover coordinator to appoint a new management wallet. */ + struct SetManagement_input { - uint64 claimedAmount; + /** @brief New wallet that will receive management privileges. */ + id management; }; - struct upgradeTier_input + struct SetManagement_output { - uint32 newTierLevel; + /** @brief Result code describing whether the management update succeeded. */ + EAuctionError errorCode; }; - struct upgradeTier_output + /** @brief Input payload used by the takeover coordinator or management to configure the execution fee reserve guard. */ + struct SetFeeReserveGuardConfig_input { + /** @brief Drop in the execution fee reserve, relative to the window baseline, that triggers an emergency pause, in basis points. */ + uint64 dropBasisPoints; + /** @brief Rolling window used to evaluate the execution fee reserve drop, in seconds. */ + uint64 windowSeconds; }; - struct TransferShareManagementRights_input + struct SetFeeReserveGuardConfig_output { - Asset asset; - sint64 numberOfShares; - uint32 newManagingContractIndex; + /** @brief Result code describing whether the guard configuration update succeeded. */ + EAuctionError errorCode; }; - struct TransferShareManagementRights_output + + /** @brief Input payload used by the takeover coordinator or management to manually pause or resume auction interactions. */ + struct SetEmergencyPause_input { - sint64 transferredNumberOfShares; + /** @brief Set to `1` to activate the emergency pause or `0` to resume normal operation. */ + uint8 paused; }; - struct getStats_input + struct SetEmergencyPause_output { - + /** @brief Result code describing whether the emergency pause update succeeded. */ + EAuctionError errorCode; }; - struct getStats_output + /** @brief Input payload used to fetch one auction from storage. */ + struct GetAuctionByIndex_input { - uint64 epochRevenue, totalPoolWeight; - uint32 numberOfRegister, numberOfCreatedProject, numberOfFundraising; + /** @brief Monotonic index of the auction to read. */ + uint64 auctionIndex; }; - struct getTierLevelByUser_input + /** @brief Auction data returned by the read-only auction getter. */ + struct GetAuctionByIndex_output { - id userId; + /** @brief Serializable auction data stored for the requested auction. */ + AuctionView auction; + + /** @brief Flag indicating whether the auction record exists. */ + uint8 found; }; - struct getTierLevelByUser_output + /** @brief Input payload used to fetch one participant record from an auction. */ + struct GetAuctionParticipant_input { - uint8 tierLevel; + /** @brief Monotonic index of the auction that owns the participant record. */ + uint64 auctionIndex; + + /** @brief Wallet whose participant record should be returned. */ + id participant; }; - struct getUserVoteStatus_input + /** @brief Participant data returned by the read-only participant getter. */ + struct GetAuctionParticipant_output { - id userId; + /** @brief Participant record for the requested wallet in the requested auction. */ + AuctionParticipantData participantData; + + /** @brief Flag indicating whether the participant record exists. */ + uint8 found; }; - struct getUserVoteStatus_output + /** @brief Input payload used to query the remaining post-BEGIN_EPOCH auction launch pause. */ + using GetTicksBeforeAuctionLaunch_input = NoData; + + /** @brief Result returned by the auction launch pause getter. */ + struct GetTicksBeforeAuctionLaunch_output { - uint32 numberOfVotedProjects; - Array projectIndexList; + /** @brief Number of ticks remaining before auction interactions resume after `BEGIN_EPOCH`. */ + uint32 ticks; }; - struct checkTokenCreatability_input + /** @brief Input payload used to read the current auction fee configuration. */ + /** @brief Input payload used to read the amount of accumulated service fees awaiting distribution at `END_EPOCH`. */ + using GetPendingServiceFeePool_input = NoData; + + struct GetPendingServiceFeePool_output { - uint64 tokenName; + /** @brief Aggregate fee amount still awaiting `END_EPOCH` settlement. */ + uint64 pendingServiceFeePool; }; - struct checkTokenCreatability_output + /** @brief Input payload used to inspect the detailed shared Nostromo fee pool. */ + using GetNostromoFeePool_input = NoData; + + struct GetNostromoFeePool_output { - bit result; // result = 1 is the token already issued by SC + /** @brief Detailed fee accumulators that have not yet been moved to dividends or recipient payout liabilities. */ + NostromoFeePool feePool; + + /** @brief Aggregate of every amount in `feePool`. */ + uint64 totalAmount; }; - struct getNumberOfInvestedProjects_input + /** @brief Input used to inspect a wallet's registered QU payout. */ + struct GetPendingPayout_input { - id userId; + /** @brief Wallet whose unpaid QU amount should be returned. */ + id account; }; - struct getNumberOfInvestedProjects_output + struct GetPendingPayout_output { - uint32 numberOfInvestedProjects; + /** @brief QU currently owed to the requested wallet. */ + uint64 amount; }; -protected: + /** @brief Input payload used to read the current state of the execution fee reserve guard. */ + using GetFeeReserveGuardState_input = NoData; - struct registerInTier_locals + struct GetFeeReserveGuardState_output { - uint64 tierStakedAmount; - uint32 poolWeight; + /** @brief Live execution fee reserve value read from the system contract. */ + sint64 currentFeeReserve; + + /** @brief Execution fee reserve value recorded at the start of the current guard window. */ + sint64 feeReserveBaseline; + + /** @brief Start of the current guard window; invalid when the window has not been initialized. */ + DateAndTime feeReserveBaselineAt; + + /** @brief Timestamp at which the emergency pause was triggered; invalid when not paused. */ + DateAndTime emergencyPausedAt; + + /** @brief Configured drop in the execution fee reserve that triggers an emergency pause, in basis points. */ + uint64 dropBasisPoints; + + /** @brief Configured rolling window used to evaluate the execution fee reserve drop, in seconds. */ + uint64 windowSeconds; + + /** @brief Flag indicating whether an emergency pause is currently blocking auction interactions. */ + uint8 isEmergencyPaused; }; - PUBLIC_PROCEDURE_WITH_LOCALS(registerInTier) - { - if (state.get().users.contains(qpi.invocator())) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - if (input.tierLevel < 1 || input.tierLevel > 5) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - - switch (input.tierLevel) - { - case 1: - locals.tierStakedAmount = NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT; - locals.poolWeight = NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT; - break; - case 2: - locals.tierStakedAmount = NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT; - locals.poolWeight = NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; - break; - case 3: - locals.tierStakedAmount = NOSTROMO_TIER_DOG_STAKE_AMOUNT; - locals.poolWeight = NOSTROMO_TIER_DOG_POOL_WEIGHT; - break; - case 4: - locals.tierStakedAmount = NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT; - locals.poolWeight = NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; - break; - case 5: - locals.tierStakedAmount = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT; - locals.poolWeight = NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; - break; - default: - break; - } - if (qpi.invocationReward() < (sint64)locals.tierStakedAmount) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - else - { - state.mut().users.set(qpi.invocator(), input.tierLevel); - state.mut().numberOfRegister++; - if (qpi.invocationReward() > (sint64)locals.tierStakedAmount) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.tierStakedAmount); - } - state.mut().totalPoolWeight += locals.poolWeight; - output.tierLevel = input.tierLevel; - } - } + using GetAuctionFees_input = NoData; - struct logoutFromTier_locals + struct GetAuctionFees_output { - uint64 earnedAmount; - uint32 elementIndex; - uint8 tierLevel; + /** @brief Fee charged when a private auction is created. */ + sint64 privateAuctionFee; + + /** @brief Non-negative fee accumulated when a public auction is created and distributed at `END_EPOCH`. */ + sint64 publicAuctionCreationFee; + + /** @brief Cancellation fee rate in basis points. */ + uint64 auctionCancellationFeeBasisPoints; + + /** @brief Management fee rate in basis points. */ + uint64 managementFeeBasisPoints; + + /** @brief Development fee rate in basis points. */ + uint64 developmentFeeBasisPoints; + + /** @brief Takeover coordinator fee rate in basis points. */ + uint64 takeoverCoordinatorFeeBasisPoints; + + /** @brief Percentage of the shareholder fee distributed as dividends, in basis points. */ + uint64 shareholderDividendBasisPoints; + + /** @brief Shareholder fee tier for auctions up to the first threshold. */ + uint64 shareholderFeeBasisPointsTier1; + + /** @brief Shareholder fee tier for auctions above the first threshold and up to the second threshold. */ + uint64 shareholderFeeBasisPointsTier2; + + /** @brief Shareholder fee tier for auctions above the second threshold and up to the third threshold. */ + uint64 shareholderFeeBasisPointsTier3; + + /** @brief Shareholder fee tier for auctions above the third threshold. */ + uint64 shareholderFeeBasisPointsTier4; }; - PUBLIC_PROCEDURE_WITH_LOCALS(logoutFromTier) + /** @brief Input for the arithmetic-only Batch Auction bid reward calculator. */ + struct CalculateBatchAuctionBidFee_input { - if (state.get().users.contains(qpi.invocator()) == 0) - { - return ; - } - state.get().users.get(qpi.invocator(), locals.tierLevel); - switch (locals.tierLevel) - { - case 1: - locals.earnedAmount = div(NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT * NOSTROMO_TIER_FACEHUGGER_UNSTAKE_FEE, 100ULL); - qpi.transfer(qpi.invocator(), qpi.invocationReward() + NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT - locals.earnedAmount); - state.mut().epochRevenue += locals.earnedAmount; - state.mut().totalPoolWeight -= NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT; - break; - case 2: - locals.earnedAmount = div(NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT * NOSTROMO_TIER_CHESTBURST_UNSTAKE_FEE, 100ULL); - qpi.transfer(qpi.invocator(), qpi.invocationReward() + NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT - locals.earnedAmount); - state.mut().epochRevenue += locals.earnedAmount; - state.mut().totalPoolWeight -= NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; - break; - case 3: - locals.earnedAmount = div(NOSTROMO_TIER_DOG_STAKE_AMOUNT * NOSTROMO_TIER_DOG_UNSTAKE_FEE, 100ULL); - qpi.transfer(qpi.invocator(), qpi.invocationReward() + NOSTROMO_TIER_DOG_STAKE_AMOUNT - locals.earnedAmount); - state.mut().epochRevenue += locals.earnedAmount; - state.mut().totalPoolWeight -= NOSTROMO_TIER_DOG_POOL_WEIGHT; - break; - case 4: - locals.earnedAmount = div(NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT * NOSTROMO_TIER_XENOMORPH_UNSTAKE_FEE, 100ULL); - qpi.transfer(qpi.invocator(), qpi.invocationReward() + NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT - locals.earnedAmount); - state.mut().epochRevenue += locals.earnedAmount; - state.mut().totalPoolWeight -= NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; - break; - case 5: - locals.earnedAmount = div(NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT * NOSTROMO_TIER_WARRIOR_UNSTAKE_FEE, 100ULL); - qpi.transfer(qpi.invocator(), qpi.invocationReward() + NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT - locals.earnedAmount); - state.mut().epochRevenue += locals.earnedAmount; - state.mut().totalPoolWeight -= NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; - break; - default: - break; - } + /** @brief Number of assets requested by the prospective bid. */ + uint64 bidQuantity; - state.mut().users.removeByKey(qpi.invocator()); - state.mut().numberOfRegister -= 1; - output.result = 1; - } + /** @brief Prospective price per asset, in qu. */ + uint64 bidAmount; + }; - struct createProject_locals + /** @brief Escrow, accumulated fee, and total reward required by the Batch Auction bid arithmetic. */ + struct CalculateBatchAuctionBidFee_output { - projectInfo newProject; - uint32 elementIndex, startDate, endDate, curDate; - uint8 tierLevel; + /** @brief Saturating product of `bidQuantity` and `bidAmount`. */ + uint64 escrowAmount; + + /** @brief Amount accumulated for distribution at `END_EPOCH` for an accepted bid: `max(100 - bidQuantity * bidAmount, 0)`. */ + uint64 fee; + + /** @brief Saturating sum of `escrowAmount` and `fee`. */ + uint64 requiredReward; }; - PUBLIC_PROCEDURE_WITH_LOCALS(createProject) + /** + * @brief Pure breakdown of one auction revenue split. + * @note Runtime settlement and tests share this struct to keep fee arithmetic aligned. + */ + struct AuctionRevenueBreakdown { - packNostromoDate(input.startYear, input.startMonth, input.startDay, input.startHour, 0, 0, locals.startDate); - packNostromoDate(input.endYear, input.endMonth, input.endDay, input.endHour, 0, 0, locals.endDate); - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); + /** @brief Net amount that remains for the seller after every configured fee is applied. */ + uint64 sellerPayout; - if(locals.curDate > locals.startDate || locals.startDate >= locals.endDate || checkValidNostDateTime(locals.startDate) == 0 || checkValidNostDateTime(locals.endDate) == 0) - { - output.indexOfProject = NOSTROMO_MAX_NUMBER_PROJECT; - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } + /** @brief Shareholder fee tier selected for the provided gross amount. */ + uint64 shareholderFeeBasisPoints; - if (state.get().tokens.contains(input.tokenName)) - { - output.indexOfProject = NOSTROMO_MAX_NUMBER_PROJECT; - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } + /** @brief Gross shareholder fee amount before dividend retention is split out. */ + uint64 shareholderFeeAmount; - if (state.get().users.get(qpi.invocator(), locals.tierLevel) && (locals.tierLevel == 4 || locals.tierLevel == 5)) - { - if (qpi.invocationReward() < NOSTROMO_CREATE_PROJECT_FEE) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.indexOfProject = NOSTROMO_MAX_NUMBER_PROJECT; - return ; + /** @brief Portion of the shareholder fee retained by the contract for dividend distribution. */ + uint64 shareholderDividendAmount; + + /** @brief Management wallet fee amount. */ + uint64 managementFeeAmount; + + /** @brief Development wallet fee amount. */ + uint64 developmentFeeAmount; + + /** @brief Base takeover coordinator fee charged directly from the gross amount. */ + uint64 takeoverCoordinatorBaseAmount; + + /** @brief Total takeover coordinator gain including retained shareholder-fee remainder. */ + uint64 takeoverCoordinatorFeeAmount; + }; + + /** + * @brief Pure breakdown of one service fee charged for private-auction creation or auction cancellation. + * @note Runtime settlement and tests share this struct to keep fee arithmetic aligned. + */ + struct AuctionServiceFeeBreakdown + { + /** @brief Portion retained by the contract for shareholder dividends. */ + uint64 shareholderDividendAmount; + + /** @brief Management wallet fee amount. */ + uint64 managementFeeAmount; + + /** @brief Development wallet fee amount. */ + uint64 developmentFeeAmount; + + /** @brief Takeover coordinator wallet fee amount. */ + uint64 takeoverCoordinatorFeeAmount; + }; + + /** @brief Input payload used to read the wallets that receive auction fee transfers. */ + using GetFeeRecipients_input = NoData; + + struct GetFeeRecipients_output + { + /** @brief Wallet that receives the management fee. */ + id management; + + /** @brief Wallet that receives the development fee. */ + id development; + + /** @brief Wallet that receives the takeover coordinator fee. */ + id takeoverCoordinator; + }; + + /** @brief Input payload used to read the closed auctions history ring buffer. */ + using GetClosedAuctionHistory_input = NoData; + + struct GetClosedAuctionHistory_output + { + /** @brief Ring buffer of auction indices recorded after finalization or cancellation. */ + Array auctionIndices; + + /** @brief Total number of history writes since initialization. */ + uint64 totalEntries; + }; + + /** @brief Input payload used to read the temporary fee routing override flag. */ + using GetRouteAllFeesToDevelopment_input = NoData; + + struct GetRouteAllFeesToDevelopment_output + { + /** @brief `1` routes every fee to development, `0` uses the standard fee distribution. */ + uint8 enabled; + }; + + struct AuctionSummary + { + Array metadataIpfsCid; + id seller; + id highestBidder; + DateAndTime createdAt; + DateAndTime settledAt; + uint64 auctionIndex; + uint64 quantityForSale; + uint64 allocatedQuantity; + uint64 initialPrice; + uint64 salePrice; + uint64 buyNowPrice; + uint64 highestBidPrice; + uint64 highestBidQuantity; + uint64 highestBidAmount; + uint8 type; + uint8 visibility; + uint8 status; + }; + + struct ParticipantSummary + { + id participant; + DateAndTime lastBidTime; + uint64 bidAmount; + uint64 escrowedAmount; + uint64 requestedQuantity; + uint64 allocatedQuantity; + uint8 isWinningBid; + }; + + struct UserParticipationSummary + { + id participant; + DateAndTime lastBidTime; + uint64 auctionIndex; + uint64 bidAmount; + uint64 escrowedAmount; + uint64 requestedQuantity; + uint64 allocatedQuantity; + uint8 isWinningBid; + }; + + struct ContractStats + { + uint64 totalAuctionsCreated; + uint64 activeAuctionCount; + uint64 pendingSellerDecisionAuctionCount; + uint64 finalizedAuctionCount; + uint64 cancelledAuctionCount; + uint64 participantCount; + uint64 closedAuctionHistoryCounter; + uint64 auctionShareholderDividendPool; + uint64 pendingServiceFeePool; + uint64 totalPendingQuPayouts; + uint64 retainedClosedAuctionCount; + uint64 retainedParticipantHistoryCount; + uint32 qxTransferFee; + uint8 routeAllFeesToDevelopment; + uint8 isAuctionTimerPaused; + uint8 isPostBeginEpochPauseArmed; + uint8 isEmergencyPaused; + }; + + using GetContractStats_input = NoData; + struct GetContractStats_output + { + ContractStats stats; + }; + + struct GetAuctionSummaries_input + { + uint64 offset; + uint64 limit; + }; + struct GetAuctionSummaries_output + { + Array auctions; + uint64 totalCount; + uint64 returnedCount; + }; + + struct GetActiveAuctionIndices_input + { + uint64 offset; + uint64 limit; + }; + struct GetActiveAuctionIndices_output + { + Array auctionIndices; + uint64 totalCount; + uint64 returnedCount; + }; + + struct GetAuctionsBySeller_input + { + id seller; + uint64 offset; + uint64 limit; + }; + struct GetAuctionsBySeller_output + { + Array auctions; + uint64 totalCount; + uint64 returnedCount; + }; + + struct GetAuctionByMetadataCid_input + { + Array metadataIpfsCid; + }; + struct GetAuctionByMetadataCid_output + { + AuctionSummary auction; + uint64 auctionIndex; + uint8 found; + }; + + struct GetAuctionSummariesByIndexBatch_input + { + Array auctionIndices; + uint64 count; + }; + struct GetAuctionSummariesByIndexBatch_output + { + Array auctions; + Array found; + uint64 returnedCount; + }; + + struct GetAuctionParticipants_input + { + uint64 auctionIndex; + uint64 offset; + uint64 limit; + }; + struct GetAuctionParticipants_output + { + Array participants; + uint64 totalCount; + uint64 returnedCount; + }; + + struct GetUserParticipations_input + { + id participant; + uint64 offset; + uint64 limit; + }; + struct GetUserParticipations_output + { + Array participations; + uint64 totalCount; + uint64 returnedCount; + }; + + using GetLatestAuctionIndex_input = NoData; + struct GetLatestAuctionIndex_output + { + uint64 auctionIndex; + uint8 found; + }; + + struct GetAuctionCountBySeller_input + { + id seller; + }; + struct GetAuctionCountBySeller_output + { + uint64 count; + }; + + struct GetAuctionAtCreationSnapshot_input + { + uint64 auctionIndex; + }; + struct GetAuctionAtCreationSnapshot_output + { + id seller; + DateAndTime createdAt; + uint64 auctionIndex; + uint64 quantityForSale; + uint64 initialPrice; + uint64 salePrice; + uint64 minimumBidIncrement; + uint64 buyNowPrice; + uint64 auctionDurationSeconds; + uint8 type; + uint8 visibility; + uint8 found; + }; + + /** @brief Input payload used to read current bid capacity guidance for one active Batch Auction. */ + struct GetBatchAuctionBidAvailability_input + { + /** @brief Monotonic index of the Batch Auction to inspect. */ + uint64 auctionIndex; + }; + + /** @brief Read-only guidance for the next acceptable Batch Auction bid. */ + struct GetBatchAuctionBidAvailability_output + { + /** @brief Lowest price per asset that can currently accept a new bid meeting the auction minimum quantity. */ + uint64 minimumBidPrice; + + /** @brief Quantity available at `minimumBidPrice`; zero when no valid new bid can be accepted. */ + uint64 availableQuantity; + + /** @brief Flag indicating whether the auction exists. */ + uint8 found; + + /** @brief Flag indicating whether the auction is an active Batch Auction that can accept another valid bid. */ + uint8 isAcceptingBids; + }; + + /** @brief Internal input used to validate an auction lot and resolve its total escrow quantity. */ + struct AnalyzeAuctionLot_input + { + /** @brief Auction lot contents to validate. */ + Array auctionLotItems; + + /** @brief Requested auction duration in days. */ + uint32 durationDays; + }; + + /** @brief Internal output returned after validating an auction lot. */ + struct AnalyzeAuctionLot_output + { + /** @brief Total quantity that must be escrowed from the lot. */ + uint64 totalEscrowQuantity; + + /** @brief Number of non-empty lot entries found in the lot. */ + uint64 lotItemCount; + + /** @brief Flag indicating whether the lot and duration are valid. */ + uint8 isValid; + }; + + struct AnalyzeAuctionLot_locals + { + AuctionAssetEntry lotItem; + uint64 lotItemIndex; + }; + + /** @brief Internal input used to count non-empty wallet entries in a private wallet whitelist. */ + struct CountAllowedBidderWallets_input + { + /** @brief Wallet list provided for private wallet-based access control. */ + Array allowedBidderWallets; + }; + + /** @brief Internal output containing the number of non-empty wallet whitelist entries. */ + struct CountAllowedBidderWallets_output + { + /** @brief Number of non-zero wallet entries found in the whitelist. */ + uint64 allowedWalletCount; + }; + + struct CountAllowedBidderWallets_locals + { + uint64 allowedWalletIndex; + }; + + /** @brief Internal input used to count non-empty asset entries in a private asset access list. */ + struct CountRequiredAccessAssets_input + { + /** @brief Asset and minimum-quantity alternatives provided for private access control. */ + Array requiredAccessAssets; + }; + + /** @brief Internal output containing the number of non-empty private access assets. */ + struct CountRequiredAccessAssets_output + { + /** @brief Number of populated asset entries found in the private access list. */ + uint64 requiredAccessAssetCount; + + /** @brief Flag indicating whether every entry has a valid asset/quantity combination. */ + uint8 isValid; + }; + + struct CountRequiredAccessAssets_locals + { + AuctionAssetEntry requiredAccessAsset; + uint64 requiredAccessAssetIndex; + }; + + struct NostromoProcedureLog + { + uint32 contractIndex; + uint32 errorCode; + id actor; + sint64 amount; + uint64 auctionIndex; + uint8 procedure; + sint8 _terminator; + }; + + /** @brief Internal input used to locate either a live or retained closed auction. */ + struct FindAuction_input + { + uint64 auctionIndex; + }; + + struct FindAuction_output + { + AuctionData auction; + uint8 found; + }; + + struct FindAuction_locals + { + AuctionData archivedAuction; + uint64 historyIndex; + }; + + /** @brief Internal input used to test whether an auction remains in retained closed history. */ + struct IsClosedAuctionRetained_input + { + uint64 auctionIndex; + }; + + struct IsClosedAuctionRetained_output + { + uint8 found; + }; + + struct IsClosedAuctionRetained_locals + { + uint64 retainedClosedAuctionCount; + uint64 historyIndex; + }; + + /** @brief Internal cursor used to enumerate retained auctions in ascending creation order. */ + struct SelectNextRetainedAuction_input + { + id seller; + uint64 afterAuctionIndex; + uint8 hasAfterAuctionIndex; + uint8 includeClosedAuctions; + uint8 filterBySeller; + }; + + struct SelectNextRetainedAuction_output + { + AuctionData auction; + uint8 found; + }; + + struct SelectNextRetainedAuction_locals + { + AuctionData candidateAuction; + uint64 retainedClosedAuctionCount; + uint64 historyIndex; + sint64 auctionElementIndex; + }; + + struct CountRetainedAuctionsBySeller_input + { + id seller; + }; + + struct CountRetainedAuctionsBySeller_output + { + uint64 count; + }; + + struct CountRetainedAuctionsBySeller_locals + { + AuctionData candidateAuction; + uint64 retainedClosedAuctionCount; + uint64 historyIndex; + sint64 auctionElementIndex; + }; + + struct FindFirstRetainedAuctionByMetadataCid_input + { + Array metadataIpfsCid; + }; + + struct FindFirstRetainedAuctionByMetadataCid_output + { + AuctionData auction; + uint8 found; + }; + + struct FindFirstRetainedAuctionByMetadataCid_locals + { + AuctionData candidateAuction; + uint64 retainedClosedAuctionCount; + uint64 metadataIndex; + uint64 historyIndex; + sint64 auctionElementIndex; + uint8 metadataMatches; + }; + + struct GetAuctionByIndex_locals + { + AuctionData auction; + FindAuction_input findAuctionInput; + FindAuction_output findAuctionOutput; + AuctionAssetEntry requiredAccessAsset; + id allowedBidderWallet; + sint64 requiredAccessAssetSetIndex; + sint64 allowedBidderWalletSetIndex; + }; + + struct GetterScan_locals + { + AuctionData auction; + AuctionParticipantData participantData; + AuctionSummary auctionSummary; + ParticipantSummary participantSummary; + UserParticipationSummary userParticipationSummary; + SelectNextRetainedAuction_input selectNextAuctionInput; + SelectNextRetainedAuction_output selectNextAuctionOutput; + FindAuction_input findAuctionInput; + FindAuction_output findAuctionOutput; + uint64 auctionIndex; + uint64 boundedLimit; + uint64 metadataIndex; + uint64 requestedIndex; + uint64 participantSlotIndex; + uint64 historyIndex; + uint64 scannedAuctionCount; + sint64 auctionElementIndex; + uint8 metadataMatches; + }; + + using GetContractStats_locals = GetterScan_locals; + + struct GetAuctionSummaries_locals + { + AuctionData auction; + AuctionSummary auctionSummary; + SelectNextRetainedAuction_input selectNextAuctionInput; + SelectNextRetainedAuction_output selectNextAuctionOutput; + uint64 boundedLimit; + uint64 scannedAuctionCount; + }; + + struct GetActiveAuctionIndices_locals + { + SelectNextRetainedAuction_input selectNextAuctionInput; + SelectNextRetainedAuction_output selectNextAuctionOutput; + uint64 boundedLimit; + uint64 scannedAuctionCount; + }; + + struct GetAuctionsBySeller_locals + { + AuctionData auction; + AuctionSummary auctionSummary; + SelectNextRetainedAuction_input selectNextAuctionInput; + SelectNextRetainedAuction_output selectNextAuctionOutput; + CountRetainedAuctionsBySeller_input countAuctionsInput; + CountRetainedAuctionsBySeller_output countAuctionsOutput; + uint64 boundedLimit; + uint64 scannedAuctionCount; + }; + + struct GetAuctionByMetadataCid_locals + { + FindFirstRetainedAuctionByMetadataCid_input findAuctionInput; + FindFirstRetainedAuctionByMetadataCid_output findAuctionOutput; + }; + + using GetAuctionSummariesByIndexBatch_locals = GetterScan_locals; + using GetAuctionParticipants_locals = GetterScan_locals; + using GetUserParticipations_locals = GetterScan_locals; + + struct GetAuctionCountBySeller_locals + { + CountRetainedAuctionsBySeller_input countAuctionsInput; + CountRetainedAuctionsBySeller_output countAuctionsOutput; + }; + + using GetAuctionAtCreationSnapshot_locals = GetterScan_locals; + + struct GetAuctionParticipant_locals + { + AuctionParticipantData participantData; + uint64 participantSlotIndex; + uint64 bestParticipantSlotIndex; + uint8 bestParticipantFound; + }; + + struct GetClosedAuctionHistory_locals + { + AuctionData auction; + uint64 historyIndex; + }; + + /** @brief Internal input used to compute Batch Auction capacity at a candidate bid price. */ + struct ComputeBatchBidAvailability_input + { + /** @brief Monotonic index of the Batch Auction to inspect. */ + uint64 auctionIndex; + + /** @brief Candidate bid price; zero returns capacity at the computed minimum valid price. */ + uint64 bidAmount; + }; + + using ComputeBatchBidAvailability_output = GetBatchAuctionBidAvailability_output; + + struct ComputeBatchBidAvailability_locals + { + AuctionData auction; + AuctionParticipantData participantData; + uint64 lowestWinningPrice; + uint64 outputPrice; + uint64 priorityQuantity; + uint64 salePriorityQuantity; + uint64 effectiveCoverageQuantity; + uint64 participantIndex; + uint8 lowestWinningPriceFound; + }; + + struct GetBatchAuctionBidAvailability_locals + { + ComputeBatchBidAvailability_input computeBatchBidAvailabilityInput; + IsClosedAuctionRetained_input isClosedAuctionRetainedInput; + IsClosedAuctionRetained_output isClosedAuctionRetainedOutput; + }; + + /** @brief Internal input used to verify whether the invocator satisfies any private asset requirement. */ + struct HasRequiredAccessAsset_input + { + /** @brief Monotonic index of the auction whose private asset-based access rules should be evaluated. */ + uint64 auctionIndex; + }; + + /** @brief Internal output of the private asset access check. */ + struct HasRequiredAccessAsset_output + { + /** @brief Flag indicating whether the invocator owns the minimum quantity of any required asset. */ + uint8 hasRequiredAccessAsset; + }; + + struct HasRequiredAccessAsset_locals + { + AuctionData auction; + AuctionAssetEntry requiredAccessAsset; + sint64 requiredAccessAssetSetIndex; + sint64 possessedAccessShares; + }; + + /** @brief Internal input used to settle a batch auction after its bidding window closes. */ + struct FinalizeBatchAuction_input + { + /** @brief Timestamp used as the auction settlement time. */ + DateAndTime currentDate; + + /** @brief Monotonic index of the batch auction to finalize. */ + uint64 auctionIndex; + }; + + /** @brief Internal output returned after batch auction finalization. */ + struct FinalizeBatchAuction_output + { + /** + * @brief Flag indicating whether batch settlement finished successfully. + * @note Final allocations are never smaller than the auction minimum; any insufficient remainder is returned to the seller. + */ + uint8 success; + }; + + /** @brief Internal input used to settle a standard auction when it is accepted or auto-finalized. */ + struct FinalizeStandardAuction_input + { + /** @brief Timestamp used as the auction settlement time. */ + DateAndTime currentDate; + + /** @brief Monotonic index of the standard auction to finalize. */ + uint64 auctionIndex; + }; + + /** @brief Internal output returned after standard auction finalization. */ + struct FinalizeStandardAuction_output + { + /** @brief Flag indicating whether standard auction settlement finished successfully. */ + uint8 success; + }; + + /** @brief Internal input used to reject a pending standard auction during the seller decision window. */ + struct RejectStandardAuction_input + { + /** @brief Timestamp used as the auction settlement time. */ + DateAndTime currentDate; + + /** @brief Monotonic index of the pending standard auction to reject. */ + uint64 auctionIndex; + }; + + /** @brief Internal output returned after rejecting a pending standard auction. */ + struct RejectStandardAuction_output + { + /** @brief Amount refunded to the highest bidder after the rejection. */ + uint64 refundedAmount; + + /** @brief Flag indicating whether the rejection flow finished successfully. */ + uint8 success; + }; + + /** @brief Internal input used to evaluate whether auction interactions are currently paused. */ + struct IsAuctionInteractionPaused_input + { + }; + + /** @brief Internal output of the auction interaction pause check. */ + struct IsAuctionInteractionPaused_output + { + /** @brief Flag indicating whether auction interactions are blocked by bootstrap time or by epoch timing pauses. */ + uint8 isPaused; + }; + + /** @brief Internal input used to resolve the currently active global auction pause interval. */ + struct GetAuctionPauseState_input + { + }; + + /** @brief Internal output describing the current global auction pause interval. */ + struct GetAuctionPauseState_output + { + /** @brief Pause start timestamp for the current active pause interval. */ + DateAndTime pauseStartedAt; + + /** @brief Pause end timestamp for the current active pause interval. */ + DateAndTime pauseEndsAt; + + /** @brief Flag indicating whether auction timers are currently paused. */ + uint8 isPaused; + }; + + /** @brief Internal locals used to resolve the currently active global auction pause interval. */ + struct GetAuctionPauseState_locals + { + /** @brief Compact current date marker used to detect the bootstrap default time sentinel. */ + DateAndTime currentDate; + uint32 currentDateStamp; + }; + + using SyncAuctionPauseState_input = NoData; + using SyncAuctionPauseState_output = NoData; + + /** @brief Internal locals used to synchronize auction deadlines with the global pause interval. */ + struct SyncAuctionPauseState_locals + { + AuctionData auction; + DateAndTime currentDate; + GetAuctionPauseState_input getAuctionPauseStateInput; + GetAuctionPauseState_output getAuctionPauseStateOutput; + uint64 pausedSeconds; + sint64 auctionIndex; + }; + + /** @brief Internal input used to split auction proceeds between seller and configured fee recipients. */ + struct DistributeAuctionRevenue_input + { + /** @brief Seller wallet that receives the net proceeds. */ + id seller; + + /** @brief Gross amount collected from the auction before fee distribution. */ + uint64 grossAmount; + }; + + /** @brief Internal output returned after auction revenue distribution is computed. */ + struct DistributeAuctionRevenue_output + { + /** @brief Net amount that should be transferred to the seller after auction fees. */ + uint64 sellerPayout; + + /** @brief Flag indicating whether the revenue distribution completed successfully. */ + uint8 success; + }; + + /** @brief Internal input used to accrue a service fee in the shared Nostromo fee pool. */ + struct AccumulateAuctionServiceFee_input + { + /** @brief Fee amount that should be accumulated. */ + uint64 feeAmount; + }; + + /** @brief Internal output returned after service-fee accrual is completed. */ + struct AccumulateAuctionServiceFee_output + { + /** @brief Flag indicating whether the service fee was recorded. */ + uint8 success; + }; + + using DistributeNostromoFeePool_input = NoData; + + struct DistributeNostromoFeePool_output + { + /** @brief Flag indicating whether every current pool accumulator was durably settled. */ + uint8 success; + }; + + /** @brief Internal input used to compute the remaining post-BEGIN_EPOCH launch pause. */ + struct GetTicksBeforeAuctionLaunchInternal_input + { + }; + + /** @brief Internal output containing the remaining post-BEGIN_EPOCH launch pause. */ + struct GetTicksBeforeAuctionLaunchInternal_output + { + /** @brief Number of ticks remaining before auction interactions resume after `BEGIN_EPOCH`. */ + uint32 ticks; + }; + + struct GetTicksBeforeAuctionLaunchInternal_locals + { + DateAndTime currentDate; + DateAndTime pauseEndsAt; + uint64 remainingSeconds; + }; + + struct GetTicksBeforeAuctionLaunch_locals + { + DateAndTime currentDate; + DateAndTime pauseEndsAt; + uint64 remainingSeconds; + }; + + /** @brief Internal input used to register a QU liability before settlement side effects are committed. */ + struct QueueQuPayout_input + { + id recipient; + uint64 amount; + }; + + struct QueueQuPayout_output + { + uint8 success; + }; + + struct QueueQuPayout_locals + { + uint64 previousAmount; + uint64 updatedAmount; + sint64 payoutIndex; + }; + + /** @brief Internal input used to discharge a bounded number of QPI-sized payout chunks. */ + struct FlushQuPayout_input + { + id recipient; + uint64 maxChunks; + }; + + struct FlushQuPayout_output + { + uint64 transferredAmount; + uint64 remainingAmount; + uint8 success; + }; + + struct FlushQuPayout_locals + { + uint64 chunkAmount; + uint64 chunkIndex; + sint64 transferResult; + }; + + using ProcessPendingQuPayouts_input = NoData; + using ProcessPendingQuPayouts_output = NoData; + + /** @brief Internal locals used by the bounded round-robin pending-payout processor. */ + struct ProcessPendingQuPayouts_locals + { + FlushQuPayout_input flushQuPayoutInput; + FlushQuPayout_output flushQuPayoutOutput; + id pendingPayoutRecipient; + uint64 payoutScanIndex; + uint64 payoutTargetRecipientCount; + uint64 processedPayoutRecipientCount; + sint64 payoutElementIndex; + }; + + struct QueueAndFlushQuPayout_input + { + id recipient; + uint64 amount; + uint64 maxChunks; + }; + + struct QueueAndFlushQuPayout_output + { + uint64 transferredAmount; + uint64 remainingAmount; + uint8 success; + }; + + struct QueueAndFlushQuPayout_locals + { + QueueQuPayout_input queueQuPayoutInput; + QueueQuPayout_output queueQuPayoutOutput; + FlushQuPayout_input flushQuPayoutInput; + FlushQuPayout_output flushQuPayoutOutput; + }; + + /** @brief Internal input used to move a bid record from live storage into bounded history. */ + struct ArchiveParticipant_input + { + AuctionParticipantData participantData; + }; + + using ArchiveParticipant_output = NoData; + + struct ArchiveParticipant_locals + { + uint64 historyIndex; + }; + + /** @brief Internal input used to process a batch auction bid after the common PlaceBid checks succeed. */ + struct ProcessBatchBid_input + { + /** @brief Monotonic index of the target batch auction. */ + uint64 auctionIndex; + + /** @brief Quantity requested by the bidder in the batch auction. */ + uint64 effectiveQuantity; + + /** @brief Offered price per asset for the requested quantity in the batch auction. */ + uint64 bidAmount; + + /** @brief Timestamp of the accepted bid. */ + DateAndTime currentDate; + + /** @brief Seconds elapsed since auction creation at the moment of the bid. */ + uint64 elapsedSeconds; + }; + + /** @brief Internal input used to refresh the cached highest bid fields of one batch auction. */ + struct RecomputeBatchHighestBid_input + { + /** @brief Monotonic index of the batch auction whose cached top bid must be rebuilt. */ + uint64 auctionIndex; + }; + + using RecomputeBatchHighestBid_output = NoData; + + /** @brief Internal output returned after processing a batch auction bid. */ + struct ProcessBatchBid_output + { + /** @brief Amount that remains escrowed for the accepted batch bid. */ + uint64 escrowedAmount; + + /** @brief Amount refunded during batch bid processing. */ + uint64 refundedAmount; + + /** @brief Result code describing whether the batch bid processing succeeded. */ + EAuctionError errorCode; + + /** @brief Flag indicating whether batch bid processing completed successfully. */ + uint8 success; + }; + + struct ProcessBatchBid_locals + { + AuctionData auction; + AuctionParticipantData participantData; + AuctionParticipantData worstParticipantData; + ComputeBatchBidAvailability_input computeBatchBidAvailabilityInput; + ComputeBatchBidAvailability_output computeBatchBidAvailabilityOutput; + RecomputeBatchHighestBid_input recomputeBatchHighestBidInput; + RecomputeBatchHighestBid_output recomputeBatchHighestBidOutput; + ArchiveParticipant_input archiveParticipantInput; + ArchiveParticipant_output archiveParticipantOutput; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; + AccumulateAuctionServiceFee_input accumulateAuctionServiceFeeInput; + AccumulateAuctionServiceFee_output accumulateAuctionServiceFeeOutput; + uint64 activeQuantity; + uint64 displacedQuantity; + uint64 displacedRefund; + uint64 excessQuantity; + uint64 remainingWorstQuantity; + CalculateBatchAuctionBidFee_output bidFeeCalculation; + uint64 participantIndex; + uint64 freeParticipantSlotIndex; + uint64 worstParticipantSlotIndex; + uint8 worstParticipantFound; + uint8 freeParticipantSlotFound; + }; + + struct RecomputeBatchHighestBid_locals + { + AuctionData auction; + AuctionParticipantData participantData; + AuctionParticipantData bestParticipantData; + uint64 participantIndex; + uint64 bestParticipantSlotIndex; + uint8 bestParticipantFound; + }; + + /** @brief Internal input used to process a standard auction bid after the common PlaceBid checks succeed. */ + struct ProcessStandardBid_input + { + /** @brief Monotonic index of the target standard auction. */ + uint64 auctionIndex; + + /** @brief Total amount the bidder commits for the standard auction lot. */ + uint64 bidAmount; + + /** @brief Timestamp of the accepted bid. */ + DateAndTime currentDate; + + /** @brief Seconds elapsed since auction creation at the moment of the bid. */ + uint64 elapsedSeconds; + }; + + /** @brief Internal output returned after processing a standard auction bid. */ + struct ProcessStandardBid_output + { + /** @brief Amount that remains escrowed for the accepted standard bid. */ + uint64 escrowedAmount; + + /** @brief Amount refunded during standard bid processing. */ + uint64 refundedAmount; + + /** @brief Result code describing whether the standard bid processing succeeded. */ + EAuctionError errorCode; + + /** @brief Flag indicating whether standard bid processing completed successfully. */ + uint8 success; + }; + + struct ProcessStandardBid_locals + { + AuctionData auction; + AuctionParticipantData participantData; + AuctionParticipantData previousHighestBidderData; + FinalizeStandardAuction_input finalizeStandardAuctionInput; + FinalizeStandardAuction_output finalizeStandardAuctionOutput; + ArchiveParticipant_input archiveParticipantInput; + ArchiveParticipant_output archiveParticipantOutput; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; + uint64 previousEscrow; + uint64 requiredEscrow; + uint64 participantSlotIndex; + uint64 highestBidderSlotIndex; + uint64 freeParticipantSlotIndex; + uint8 participantExists; + uint8 highestBidderExists; + uint8 freeParticipantSlotFound; + uint8 finalizeImmediately; + }; + + /** @brief Internal input used to validate the IPFS metadata CID format required by the Auction House. */ + struct ValidateMetadataCid_input + { + /** @brief Candidate lowercase base32 CIDv1 for auction metadata stored in Pinata. */ + Array metadataIpfsCid; + }; + + /** @brief Internal output of the metadata CID validation routine. */ + struct ValidateMetadataCid_output + { + /** @brief Flag indicating whether the metadata CID has the required lowercase base32 CIDv1 format. */ + uint8 isValid; + }; + + struct ValidateMetadataCid_locals + { + uint64 cidIndex; + uint8 cidChar; + uint8 hasPayloadCharacters; + uint8 reachedTerminator; + }; + + /** @brief Internal input used to verify that the seller owns enough shares for every asset in the auction lot. */ + struct VerifyAuctionLotBalances_input + { + /** @brief Auction lot that should be checked against the seller balance. */ + Array auctionLotItems; + }; + + /** @brief Internal output of the seller balance verification routine. */ + struct VerifyAuctionLotBalances_output + { + /** @brief Flag indicating whether the seller owns enough shares for the entire lot. */ + uint8 hasEnoughBalance; + }; + + struct VerifyAuctionLotBalances_locals + { + AuctionAssetEntry lotItem; + uint64 lotItemIndex; + sint64 possessedShares; + }; + + /** @brief Internal input used to transfer the auction lot from the seller into contract escrow. */ + struct EscrowAuctionLotAssets_input + { + /** @brief Auction lot that must be moved into contract escrow. */ + Array auctionLotItems; + }; + + /** @brief Internal output of the lot escrow routine. */ + struct EscrowAuctionLotAssets_output + { + /** @brief Flag indicating whether every lot asset was successfully escrowed. */ + uint8 success; + }; + + struct EscrowAuctionLotAssets_locals + { + AuctionAssetEntry lotItem; + uint64 lotItemIndex; + uint64 rollbackLotItemIndex; + sint64 remainingShares; + }; + + /** @brief Internal input used to return an auction lot from contract escrow to a target wallet. */ + struct RollbackAuctionLotAssets_input + { + /** @brief Auction lot that must be transferred out of contract escrow. */ + Array auctionLotItems; + + /** @brief Destination wallet that should receive the lot from escrow. */ + id recipient; + }; + + using RollbackAuctionLotAssets_output = NoData; + + struct RollbackAuctionLotAssets_locals + { + AuctionAssetEntry lotItem; + uint64 lotItemIndex; + }; + + /** @brief Internal input used to archive and remove a closed auction from active storage. */ + struct ArchiveClosedAuction_input + { + AuctionData auction; + }; + + using ArchiveClosedAuction_output = NoData; + + struct ArchiveClosedAuction_locals + { + uint64 historyIndex; + }; + + struct FinalizeBatchAuction_locals + { + AuctionData auction; + AuctionParticipantData participantData; + AuctionParticipantData bestParticipantData; + AuctionAssetEntry batchLotItem; + DistributeAuctionRevenue_input distributeAuctionRevenueInput; + DistributeAuctionRevenue_output distributeAuctionRevenueOutput; + ArchiveParticipant_input archiveParticipantInput; + ArchiveParticipant_output archiveParticipantOutput; + ArchiveClosedAuction_input archiveClosedAuctionInput; + ArchiveClosedAuction_output archiveClosedAuctionOutput; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; + DateAndTime currentDate; + uint64 remainingQuantity; + uint64 allocatedQuantity; + uint64 requiredPayment; + uint64 refundAmount; + uint64 soldQuantity; + uint64 totalGrossAmount; + uint64 lotItemIndex; + uint64 participantIndex; + uint64 bestParticipantSlotIndex; + uint8 bestParticipantFound; + uint8 lotItemFound; + }; + + struct FinalizeStandardAuction_locals + { + AuctionData auction; + AuctionParticipantData highestBidderData; + RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; + RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; + DistributeAuctionRevenue_input distributeAuctionRevenueInput; + DistributeAuctionRevenue_output distributeAuctionRevenueOutput; + ArchiveParticipant_input archiveParticipantInput; + ArchiveParticipant_output archiveParticipantOutput; + ArchiveClosedAuction_input archiveClosedAuctionInput; + ArchiveClosedAuction_output archiveClosedAuctionOutput; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; + uint64 highestBidderSlotIndex; + uint8 highestBidderExists; + uint8 lotSold; + }; + + struct DistributeAuctionRevenue_locals + { + AuctionRevenueBreakdown auctionRevenueBreakdown; + NostromoFeePool feePool; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; + uint64 shareholderFeeTierIndex; + }; + + struct AccumulateAuctionServiceFee_locals + { + NostromoFeePool feePool; + }; + + struct DistributeNostromoFeePool_locals + { + AuctionServiceFeeBreakdown auctionServiceFeeBreakdown; + NostromoFeePool feePool; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; + uint64 shareholderDividendAmount; + uint64 distributedDividendAmount; + uint64 dividendPerShare; + }; + + struct RejectStandardAuction_locals + { + AuctionData auction; + AuctionParticipantData highestBidderData; + RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; + RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; + ArchiveParticipant_input archiveParticipantInput; + ArchiveParticipant_output archiveParticipantOutput; + ArchiveClosedAuction_input archiveClosedAuctionInput; + ArchiveClosedAuction_output archiveClosedAuctionOutput; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; + uint64 highestBidderSlotIndex; + uint8 highestBidderExists; + }; + + struct CreateAuction_locals + { + AuctionData auction; + NostromoProcedureLog log; + IsAuctionInteractionPaused_input isAuctionInteractionPausedInput; + IsAuctionInteractionPaused_output isAuctionInteractionPausedOutput; + ValidateMetadataCid_input validateMetadataCidInput; + ValidateMetadataCid_output validateMetadataCidOutput; + AnalyzeAuctionLot_input analyzeAuctionLotInput; + AnalyzeAuctionLot_output analyzeAuctionLotOutput; + CountAllowedBidderWallets_input countAllowedBidderWalletsInput; + CountAllowedBidderWallets_output countAllowedBidderWalletsOutput; + CountRequiredAccessAssets_input countRequiredAccessAssetsInput; + CountRequiredAccessAssets_output countRequiredAccessAssetsOutput; + AuctionAssetEntry requiredAccessAsset; + VerifyAuctionLotBalances_input verifyAuctionLotBalancesInput; + EscrowAuctionLotAssets_input escrowAuctionLotAssetsInput; + RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; + AccumulateAuctionServiceFee_input accumulateAuctionServiceFeeInput; + sint64 requiredFee; + sint64 existingRequiredAccessQuantity; + uint64 resolvedQuantityForSale; + uint64 resolvedMinimumPurchaseQuantity; + uint64 allowedWalletIndex; + uint64 requiredAccessAssetIndex; + RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; + EscrowAuctionLotAssets_output escrowAuctionLotAssetsOutput; + VerifyAuctionLotBalances_output verifyAuctionLotBalancesOutput; + AccumulateAuctionServiceFee_output accumulateAuctionServiceFeeOutput; + }; + + struct PlaceBid_locals + { + AuctionData auction; + FindAuction_input findAuctionInput; + FindAuction_output findAuctionOutput; + NostromoProcedureLog log; + IsAuctionInteractionPaused_input isAuctionInteractionPausedInput; + IsAuctionInteractionPaused_output isAuctionInteractionPausedOutput; + HasRequiredAccessAsset_input hasRequiredAccessAssetInput; + HasRequiredAccessAsset_output hasRequiredAccessAssetOutput; + ProcessBatchBid_input processBatchBidInput; + ProcessBatchBid_output processBatchBidOutput; + ProcessStandardBid_input processStandardBidInput; + ProcessStandardBid_output processStandardBidOutput; + uint64 elapsedSeconds; + DateAndTime currentDate; + uint8 hasAccess; + }; + + struct CancelAuction_locals + { + AuctionData auction; + FindAuction_input findAuctionInput; + FindAuction_output findAuctionOutput; + AuctionParticipantData participantData; + NostromoProcedureLog log; + RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; + RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; + AccumulateAuctionServiceFee_input accumulateAuctionServiceFeeInput; + AccumulateAuctionServiceFee_output accumulateAuctionServiceFeeOutput; + ArchiveClosedAuction_input archiveClosedAuctionInput; + ArchiveClosedAuction_output archiveClosedAuctionOutput; + DateAndTime currentDate; + uint64 cancellationBaseAmount; + uint64 participantIndex; + }; + + struct ResolvePendingStandardAuction_locals + { + AuctionData auction; + FindAuction_input findAuctionInput; + FindAuction_output findAuctionOutput; + DateAndTime currentDate; + NostromoProcedureLog log; + + IsAuctionInteractionPaused_input isAuctionInteractionPausedInput; + IsAuctionInteractionPaused_output isAuctionInteractionPausedOutput; + FinalizeStandardAuction_input finalizeStandardAuctionInput; + FinalizeStandardAuction_output finalizeStandardAuctionOutput; + RejectStandardAuction_input rejectStandardAuctionInput; + RejectStandardAuction_output rejectStandardAuctionOutput; + }; + + struct END_TICK_locals + { + AuctionData auction; + DateAndTime currentDate; + SyncAuctionPauseState_input syncAuctionPauseStateInput; + SyncAuctionPauseState_output syncAuctionPauseStateOutput; + uint64 elapsedSeconds; + uint32 currentDateStamp; + sint64 auctionIndex; + FinalizeBatchAuction_input finalizeBatchAuctionInput; + FinalizeBatchAuction_output finalizeBatchAuctionOutput; + FinalizeStandardAuction_input finalizeStandardAuctionInput; + FinalizeStandardAuction_output finalizeStandardAuctionOutput; + sint64 currentReserve; + sint64 reserveDrop; + uint64 guardElapsedSeconds; + uint64 guardDropThreshold; + }; + + struct BEGIN_EPOCH_locals + { + QX::Fees_input feesInput; + QX::Fees_output feesOutput; + }; + + struct END_EPOCH_locals + { + DistributeNostromoFeePool_input distributeNostromoFeePoolInput; + DistributeNostromoFeePool_output distributeNostromoFeePoolOutput; + ProcessPendingQuPayouts_input processPendingQuPayoutsInput; + ProcessPendingQuPayouts_output processPendingQuPayoutsOutput; + }; + + /** @brief Input payload used to move share management rights to another managing contract. */ + struct TransferShareManagementRights_input + { + /** @brief Asset whose management rights should be transferred. */ + Asset asset; + + /** @brief Number of shares whose management rights should be transferred. */ + sint64 numberOfShares; + + /** @brief Destination managing contract index. */ + uint32 newManagingContractIndex; + }; + + /** @brief Result of a share management rights transfer request. */ + struct TransferShareManagementRights_output + { + /** @brief Number of shares whose management rights were transferred. */ + sint64 transferredNumberOfShares; + + /** @brief Result code describing whether the transfer request succeeded. */ + EAuctionError errorCode; + }; + + struct TransferShareManagementRights_locals + { + NostromoProcedureLog log; + + sint64 result; + sint64 reward; + sint64 refundAmount; + bit success; + }; + + struct SetAuctionFees_locals + { + NostromoProcedureLog log; + }; + + struct SetAuctionFeesByManagement_locals + { + NostromoProcedureLog log; + }; + + struct SetManagement_locals + { + NostromoProcedureLog log; + }; + + struct SetFeeReserveGuardConfig_locals + { + NostromoProcedureLog log; + }; + + struct SetEmergencyPause_locals + { + NostromoProcedureLog log; + }; + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_PROCEDURE(CreateAuction, static_cast(EProcedureId::CreateAuction)); + REGISTER_USER_PROCEDURE(PlaceBid, static_cast(EProcedureId::PlaceBid)); + REGISTER_USER_PROCEDURE(CancelAuction, static_cast(EProcedureId::CancelAuction)); + REGISTER_USER_PROCEDURE(TransferShareManagementRights, static_cast(EProcedureId::TransferShareManagementRights)); + REGISTER_USER_PROCEDURE(ResolvePendingStandardAuction, static_cast(EProcedureId::ResolvePendingStandardAuction)); + REGISTER_USER_PROCEDURE(SetAuctionFees, static_cast(EProcedureId::SetAuctionFees)); + REGISTER_USER_PROCEDURE(SetAuctionFeesByManagement, static_cast(EProcedureId::SetAuctionFeesByManagement)); + REGISTER_USER_PROCEDURE(SetManagement, static_cast(EProcedureId::SetManagement)); + REGISTER_USER_PROCEDURE(SetFeeReserveGuardConfig, static_cast(EProcedureId::SetFeeReserveGuardConfig)); + REGISTER_USER_PROCEDURE(SetEmergencyPause, static_cast(EProcedureId::SetEmergencyPause)); + + REGISTER_USER_FUNCTION(GetAuctionByIndex, static_cast(EFunctionId::GetAuctionByIndex)); + REGISTER_USER_FUNCTION(GetAuctionParticipant, static_cast(EFunctionId::GetAuctionParticipant)); + REGISTER_USER_FUNCTION(GetTicksBeforeAuctionLaunch, static_cast(EFunctionId::GetTicksBeforeAuctionLaunch)); + REGISTER_USER_FUNCTION(GetAuctionFees, static_cast(EFunctionId::GetAuctionFees)); + REGISTER_USER_FUNCTION(GetFeeRecipients, static_cast(EFunctionId::GetFeeRecipients)); + REGISTER_USER_FUNCTION(GetClosedAuctionHistory, static_cast(EFunctionId::GetClosedAuctionHistory)); + REGISTER_USER_FUNCTION(GetRouteAllFeesToDevelopment, static_cast(EFunctionId::GetRouteAllFeesToDevelopment)); + REGISTER_USER_FUNCTION(GetContractStats, static_cast(EFunctionId::GetContractStats)); + REGISTER_USER_FUNCTION(GetAuctionSummaries, static_cast(EFunctionId::GetAuctionSummaries)); + REGISTER_USER_FUNCTION(GetActiveAuctionIndices, static_cast(EFunctionId::GetActiveAuctionIndices)); + REGISTER_USER_FUNCTION(GetAuctionsBySeller, static_cast(EFunctionId::GetAuctionsBySeller)); + REGISTER_USER_FUNCTION(GetAuctionByMetadataCid, static_cast(EFunctionId::GetAuctionByMetadataCid)); + REGISTER_USER_FUNCTION(GetAuctionSummariesByIndexBatch, static_cast(EFunctionId::GetAuctionSummariesByIndexBatch)); + REGISTER_USER_FUNCTION(GetAuctionParticipants, static_cast(EFunctionId::GetAuctionParticipants)); + REGISTER_USER_FUNCTION(GetUserParticipations, static_cast(EFunctionId::GetUserParticipations)); + REGISTER_USER_FUNCTION(GetLatestAuctionIndex, static_cast(EFunctionId::GetLatestAuctionIndex)); + REGISTER_USER_FUNCTION(GetAuctionCountBySeller, static_cast(EFunctionId::GetAuctionCountBySeller)); + REGISTER_USER_FUNCTION(GetAuctionAtCreationSnapshot, static_cast(EFunctionId::GetAuctionAtCreationSnapshot)); + REGISTER_USER_FUNCTION(GetBatchAuctionBidAvailability, static_cast(EFunctionId::GetBatchAuctionBidAvailability)); + REGISTER_USER_FUNCTION(CalculateBatchAuctionBidFee, static_cast(EFunctionId::CalculateBatchAuctionBidFee)); + REGISTER_USER_FUNCTION(GetPendingServiceFeePool, static_cast(EFunctionId::GetPendingServiceFeePool)); + REGISTER_USER_FUNCTION(GetFeeReserveGuardState, static_cast(EFunctionId::GetFeeReserveGuardState)); + REGISTER_USER_FUNCTION(GetPendingPayout, static_cast(EFunctionId::GetPendingPayout)); + REGISTER_USER_FUNCTION(GetNostromoFeePool, static_cast(EFunctionId::GetNostromoFeePool)); + } + + /** + * @brief Initializes default governance, fee, pause, and guard settings. + */ + INITIALIZE() + { + // Install the default governance, fee, pause, and guard configuration into the zeroed contract state. + state.mut().privateAuctionFee = NOST_DEFAULT_PRIVATE_AUCTION_FEE; + state.mut().publicAuctionCreationFee = NOST_PUBLIC_AUCTION_CREATION_FEE; + state.mut().auctionCancellationFeeBasisPoints = NOST_DEFAULT_AUCTION_CANCELLATION_FEE_BP; + state.mut().managementFeeBasisPoints = NOST_DEFAULT_AUCTION_MANAGEMENT_FEE_BP; + state.mut().developmentFeeBasisPoints = NOST_DEFAULT_AUCTION_DEVELOPMENT_FEE_BP; + state.mut().takeoverCoordinatorFeeBasisPoints = NOST_DEFAULT_AUCTION_TAKEOVER_COORDINATOR_FEE_BP; + state.mut().shareholderDividendBasisPoints = NOST_DEFAULT_AUCTION_SHAREHOLDER_DIVIDEND_BP; + state.mut().shareholderFeeBasisPointsTier1 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_1; + state.mut().shareholderFeeBasisPointsTier2 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_2; + state.mut().shareholderFeeBasisPointsTier3 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_3; + state.mut().shareholderFeeBasisPointsTier4 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4; + state.mut().maxAuctionDurationDays = NOST_AUCTION_MAX_DURATION_DAYS; + state.mut().isAuctionTimerPaused = 1; + state.mut().routeAllFeesToDevelopment = NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT; + state.mut().auctionTimerPauseStartedAt.setInvalid(); + state.mut().auctionTimerPauseEndsAt.setInvalid(); + state.mut().feeReserveGuardDropBasisPoints = NOST_DEFAULT_FEE_RESERVE_GUARD_DROP_BP; + state.mut().feeReserveGuardWindowSeconds = NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS; + state.mut().management = ID(_I, _G, _P, _Z, _X, _Q, _O, _R, _J, _Y, _Q, _P, _A, _G, _V, _A, _B, _N, _T, _N, _I, _S, _O, _Y, _T, _M, _T, _A, + _N, _M, _K, _Z, _A, _S, _T, _P, _P, _G, _Z, _O, _N, _A, _Q, _J, _X, _Q, _O, _S, _W, _Q, _O, _V, _J, _C, _K, _D); + state.mut().development = ID(_D, _Q, _V, _H, _M, _Z, _F, _C, _W, _O, _K, _M, _H, _F, _B, _H, _L, _X, _U, _I, _U, _G, _P, _P, _X, _R, _Z, _C, + _U, _V, _S, _N, _J, _F, _Z, _J, _F, _M, _Q, _M, _Y, _D, _B, _X, _E, _S, _E, _A, _T, _M, _W, _L, _K, _N, _L, _D); + state.mut().takeoverCoordinator = + ID(_X, _J, _O, _S, _N, _L, _T, _Z, _V, _V, _H, _N, _Z, _C, _B, _Y, _X, _I, _E, _V, _N, _E, _P, _P, _B, _O, _Q, _A, _W, _D, _B, _V, _G, _E, + _N, _Z, _O, _X, _S, _V, _O, _B, _K, _G, _Z, _C, _C, _F, _D, _B, _D, _M, _T, _M, _L, _C); + } + + MIGRATE() + { + state.mut().privateAuctionFee = NOST_DEFAULT_PRIVATE_AUCTION_FEE; + state.mut().publicAuctionCreationFee = NOST_PUBLIC_AUCTION_CREATION_FEE; + state.mut().auctionCancellationFeeBasisPoints = NOST_DEFAULT_AUCTION_CANCELLATION_FEE_BP; + state.mut().managementFeeBasisPoints = NOST_DEFAULT_AUCTION_MANAGEMENT_FEE_BP; + state.mut().developmentFeeBasisPoints = NOST_DEFAULT_AUCTION_DEVELOPMENT_FEE_BP; + state.mut().takeoverCoordinatorFeeBasisPoints = NOST_DEFAULT_AUCTION_TAKEOVER_COORDINATOR_FEE_BP; + state.mut().shareholderDividendBasisPoints = NOST_DEFAULT_AUCTION_SHAREHOLDER_DIVIDEND_BP; + state.mut().shareholderFeeBasisPointsTier1 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_1; + state.mut().shareholderFeeBasisPointsTier2 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_2; + state.mut().shareholderFeeBasisPointsTier3 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_3; + state.mut().shareholderFeeBasisPointsTier4 = NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4; + state.mut().maxAuctionDurationDays = NOST_AUCTION_MAX_DURATION_DAYS; + state.mut().routeAllFeesToDevelopment = NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT; + state.mut().feeReserveGuardDropBasisPoints = NOST_DEFAULT_FEE_RESERVE_GUARD_DROP_BP; + state.mut().feeReserveGuardWindowSeconds = NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS; + state.mut().management = ID(_I, _G, _P, _Z, _X, _Q, _O, _R, _J, _Y, _Q, _P, _A, _G, _V, _A, _B, _N, _T, _N, _I, _S, _O, _Y, _T, _M, _T, _A, + _N, _M, _K, _Z, _A, _S, _T, _P, _P, _G, _Z, _O, _N, _A, _Q, _J, _X, _Q, _O, _S, _W, _Q, _O, _V, _J, _C, _K, _D); + state.mut().development = ID(_D, _Q, _V, _H, _M, _Z, _F, _C, _W, _O, _K, _M, _H, _F, _B, _H, _L, _X, _U, _I, _U, _G, _P, _P, _X, _R, _Z, _C, + _U, _V, _S, _N, _J, _F, _Z, _J, _F, _M, _Q, _M, _Y, _D, _B, _X, _E, _S, _E, _A, _T, _M, _W, _L, _K, _N, _L, _D); + state.mut().takeoverCoordinator = + ID(_X, _J, _O, _S, _N, _L, _T, _Z, _V, _V, _H, _N, _Z, _C, _B, _Y, _X, _I, _E, _V, _N, _E, _P, _P, _B, _O, _Q, _A, _W, _D, _B, _V, _G, _E, + _N, _Z, _O, _X, _S, _V, _O, _B, _K, _G, _Z, _C, _C, _F, _D, _B, _D, _M, _T, _M, _L, _C); + } + + /** + * @brief Allows share acquisition without charging an additional contract fee. + */ + PRE_ACQUIRE_SHARES() + { + output.requestedFee = 0; + output.allowTransfer = true; + } + + /** + * @brief Refreshes epoch-scoped configuration and arms auction timer pauses. + */ + BEGIN_EPOCH_WITH_LOCALS() + { + // Refresh the QX fee cache once per epoch so share transfers can expose current cost guidance. + CALL_OTHER_CONTRACT_FUNCTION(QX, Fees, locals.feesInput, locals.feesOutput); + // Preserve the previous cache when QX is temporarily unavailable; a failed call must not install an undefined fee. + if (interContractCallError == NoCallError) + { + state.mut().qxTransferFee = locals.feesOutput.transferFee; + } + + // Freeze auction timers across the epoch boundary; END_TICK later accounts this pause back into deadlines. + state.mut().isPostBeginEpochPauseArmed = 1; + if (!state.get().isAuctionTimerPaused) + { + state.mut().isAuctionTimerPaused = 1; + state.mut().auctionTimerPauseStartedAt = qpi.now(); + state.mut().auctionTimerPauseEndsAt = qpi.now(); + return; + } + + if (!state.get().auctionTimerPauseStartedAt.isValid() || qpi.now() < state.get().auctionTimerPauseStartedAt) + { + state.mut().auctionTimerPauseStartedAt = qpi.now(); + } + if (!state.get().auctionTimerPauseEndsAt.isValid() || qpi.now() > state.get().auctionTimerPauseEndsAt) + { + state.mut().auctionTimerPauseEndsAt = qpi.now(); + } + } + + /** + * @brief Retries pending QU payouts, settles the shared Nostromo fee pool, and performs storage cleanup. + */ + END_EPOCH_WITH_LOCALS() + { + CALL(ProcessPendingQuPayouts, locals.processPendingQuPayoutsInput, locals.processPendingQuPayoutsOutput); + + CALL(DistributeNostromoFeePool, locals.distributeNostromoFeePoolInput, locals.distributeNostromoFeePoolOutput); + + state.mut().auctionList.cleanupIfNeeded(); + state.mut().pendingQuPayouts.cleanupIfNeeded(); + } + + /** + * @brief Advances auction lifecycle state and finalizes auctions whose deadlines elapsed. + */ + END_TICK_WITH_LOCALS() + { + makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentDateStamp); + locals.currentDate = qpi.now(); + + // The reserve guard converts a sudden execution-fee reserve drop into an emergency pause. + if (!state.get().isEmergencyPaused) + { + locals.currentReserve = qpi.queryFeeReserve(SELF_INDEX); + // The first observation establishes a baseline instead of interpreting startup state as a reserve drop. + if (!state.get().feeReserveBaselineAt.isValid()) + { + state.mut().feeReserveBaseline = locals.currentReserve; + state.mut().feeReserveBaselineAt = locals.currentDate; + } + else + { + // Subsequent observations either trigger the guard or roll the baseline into a new window. + diffDateInSecond(state.get().feeReserveBaselineAt, locals.currentDate, locals.guardElapsedSeconds); + locals.reserveDrop = state.get().feeReserveBaseline - locals.currentReserve; + if (state.get().feeReserveBaseline > 0 && locals.reserveDrop > 0) + { + locals.guardDropThreshold = + div(smul(static_cast(state.get().feeReserveBaseline), state.get().feeReserveGuardDropBasisPoints), + NOST_BASIS_POINTS_SCALE); + if (static_cast(locals.reserveDrop) >= locals.guardDropThreshold && + locals.guardElapsedSeconds <= state.get().feeReserveGuardWindowSeconds) + { + state.mut().isEmergencyPaused = 1; + state.mut().emergencyPausedAt = locals.currentDate; + state.mut().feeReserveBaselineAt.setInvalid(); + } + else if (locals.guardElapsedSeconds >= state.get().feeReserveGuardWindowSeconds) + { + state.mut().feeReserveBaseline = locals.currentReserve; + state.mut().feeReserveBaselineAt = locals.currentDate; + } + } + else if (locals.guardElapsedSeconds >= state.get().feeReserveGuardWindowSeconds) + { + state.mut().feeReserveBaseline = locals.currentReserve; + state.mut().feeReserveBaselineAt = locals.currentDate; + } + } + } + + CALL(SyncAuctionPauseState, locals.syncAuctionPauseStateInput, locals.syncAuctionPauseStateOutput); + // Lifecycle transitions must not advance while SyncAuctionPauseState still owns the global timer freeze. + if (state.get().isAuctionTimerPaused) + { + return; + } + + // Only live auctions advance after pause synchronization has extended their timers. + locals.auctionIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); + while (locals.auctionIndex != NULL_INDEX) + { + locals.auction = state.get().auctionList.value(locals.auctionIndex); + switch (locals.auction.core.status) + { + case EAuctionStatus::Active: + diffDateInSecond(locals.auction.core.createdAt, locals.currentDate, locals.elapsedSeconds); + // Only an elapsed active auction is eligible for automatic settlement or seller-decision transition. + if (locals.elapsedSeconds >= locals.auction.core.auctionDurationSeconds) + { + switch (locals.auction.core.type) + { + case EAuctionType::Batch: + locals.finalizeBatchAuctionInput.auctionIndex = locals.auction.core.auctionIndex; + locals.finalizeBatchAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeBatchAuction, locals.finalizeBatchAuctionInput, locals.finalizeBatchAuctionOutput); + break; + case EAuctionType::Standard: + // No-bid and reserve-satisfying outcomes are deterministic and need no seller approval window. + if (locals.auction.core.highestBidAmount == 0 || locals.auction.core.highestBidPrice >= locals.auction.core.salePrice) + { + locals.finalizeStandardAuctionInput.auctionIndex = locals.auction.core.auctionIndex; + locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); + } + else + { + // A funded bid below the seller's sale price requires an explicit, time-bounded seller choice. + // Below-sale standard bids enter a seller decision window instead of settling immediately. + locals.auction.core.status = EAuctionStatus::PendingSellerDecision; + locals.auction.core.sellerDecisionDeadline = locals.currentDate; + locals.auction.core.sellerDecisionDeadline.add(0, 0, 0, 0, 0, NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS); + state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); + } + break; + default: break; + }; + } + break; + case EAuctionStatus::PendingSellerDecision: + switch (locals.auction.core.type) + { + case EAuctionType::Standard: + // Expiry resolves in favor of the recorded highest bidder so the seller cannot lock escrow indefinitely. + if (locals.auction.core.sellerDecisionDeadline <= locals.currentDate) + { + locals.finalizeStandardAuctionInput.auctionIndex = locals.auction.core.auctionIndex; + locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); + } + break; + default: break; + } + break; + default: break; + } + + locals.auctionIndex = state.get().auctionList.nextElementIndex(locals.auctionIndex); + } + } + + /** + * @brief Validates auction lot entries and totals the escrowed quantity. + */ + PRIVATE_FUNCTION_WITH_LOCALS(AnalyzeAuctionLot) + { + output.totalEscrowQuantity = 0; + output.lotItemCount = 0; + output.isValid = 0; + + // Lot validation also enforces the configured maximum auction lifetime. + if (input.durationDays == 0 || input.durationDays > state.get().maxAuctionDurationDays) + { + return; + } + + // Scan the full fixed ABI array because valid entries may be followed only by zero-padded slots. + for (locals.lotItemIndex = 0; locals.lotItemIndex < input.auctionLotItems.capacity(); ++locals.lotItemIndex) + { + locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); + // A zero asset is padding only when its paired quantity is also zero. + if (isZeroAsset(locals.lotItem.asset)) + { + if (locals.lotItem.quantity != 0) + { + return; + } + continue; + } + + if (locals.lotItem.quantity <= 0) + { + return; + } + + output.lotItemCount = sadd(output.lotItemCount, 1ULL); + output.totalEscrowQuantity = sadd(output.totalEscrowQuantity, static_cast(locals.lotItem.quantity)); + } + + output.isValid = output.lotItemCount > 0 ? 1 : 0; + } + + /** + * @brief Resolves whether the current tick belongs to a scheduled auction pause window. + */ + PRIVATE_FUNCTION_WITH_LOCALS(GetAuctionPauseState) + { + output.isPaused = 0; + output.pauseStartedAt.setInvalid(); + output.pauseEndsAt.setInvalid(); + + // The initial runtime date is treated as a full-day launch pause. + locals.currentDate = qpi.now(); + makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentDateStamp); + if (locals.currentDateStamp == NOST_DEFAULT_INIT_TIME) + { + output.isPaused = 1; + output.pauseStartedAt = locals.currentDate; + output.pauseStartedAt.setTime(0, 0, 0, 0, 0); + output.pauseEndsAt = output.pauseStartedAt; + output.pauseEndsAt.addDays(1); + return; + } + + // Scheduled pre-epoch pauses keep auctions from expiring during the transition window. + if (qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()) == NOST_PRE_EPOCH_PAUSE_DAY_OF_WEEK && qpi.hour() == NOST_PRE_EPOCH_PAUSE_HOUR && + qpi.minute() >= NOST_PRE_EPOCH_PAUSE_MINUTE) + { + output.isPaused = 1; + output.pauseStartedAt = locals.currentDate; + output.pauseStartedAt.setTime(NOST_PRE_EPOCH_PAUSE_HOUR, NOST_PRE_EPOCH_PAUSE_MINUTE, 0, 0, 0); + output.pauseEndsAt = output.pauseStartedAt; + output.pauseEndsAt.add(0, 0, 0, 0, 0, NOST_AUCTION_PRE_EPOCH_PAUSE_SECONDS); + } + } + + /** + * @brief Reports whether user-facing auction interactions are currently paused. + */ + PRIVATE_FUNCTION(IsAuctionInteractionPaused) + { + // Emergency pause takes precedence over scheduled and post-epoch launch pauses. + if (state.get().isEmergencyPaused) + { + output.isPaused = 1; + return; + } + + output.isPaused = state.get().isAuctionTimerPaused; + if (output.isPaused) + { + return; + } + + output.isPaused = state.get().isPostBeginEpochPauseArmed && (qpi.tick() - qpi.initialTick()) < NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS; + } + + /** + * @brief Synchronizes timer pause state and extends affected auction deadlines. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(SyncAuctionPauseState) + { + locals.currentDate = qpi.now(); + + // While emergency pause is active, keep extending the timer pause window. + if (state.get().isEmergencyPaused) + { + if (!state.get().isAuctionTimerPaused) + { + state.mut().isAuctionTimerPaused = 1; + state.mut().auctionTimerPauseStartedAt = locals.currentDate; + state.mut().auctionTimerPauseEndsAt = locals.currentDate; + } + else + { + state.mut().auctionTimerPauseEndsAt = locals.currentDate; + } + return; + } + + CALL(GetAuctionPauseState, locals.getAuctionPauseStateInput, locals.getAuctionPauseStateOutput); + + // The launch pause can overlap the scheduled pause; merge both windows before timers resume. + if (state.get().isPostBeginEpochPauseArmed) + { + if ((qpi.tick() - qpi.initialTick()) < NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) + { + if (!state.get().isAuctionTimerPaused) + { + state.mut().isAuctionTimerPaused = 1; + state.mut().auctionTimerPauseStartedAt = locals.currentDate; + state.mut().auctionTimerPauseEndsAt = locals.currentDate; + } + else + { + if (!state.get().auctionTimerPauseStartedAt.isValid()) + { + state.mut().auctionTimerPauseStartedAt = locals.currentDate; + } + if (!state.get().auctionTimerPauseEndsAt.isValid() || locals.currentDate > state.get().auctionTimerPauseEndsAt) + { + state.mut().auctionTimerPauseEndsAt = locals.currentDate; + } + } + + if (locals.getAuctionPauseStateOutput.isPaused) + { + if (!state.get().auctionTimerPauseStartedAt.isValid() || + locals.getAuctionPauseStateOutput.pauseStartedAt < state.get().auctionTimerPauseStartedAt) + { + state.mut().auctionTimerPauseStartedAt = locals.getAuctionPauseStateOutput.pauseStartedAt; + } + if (!state.get().auctionTimerPauseEndsAt.isValid() || + locals.getAuctionPauseStateOutput.pauseEndsAt > state.get().auctionTimerPauseEndsAt) + { + state.mut().auctionTimerPauseEndsAt = locals.getAuctionPauseStateOutput.pauseEndsAt; + } + } + return; + } + + state.mut().isPostBeginEpochPauseArmed = 0; + } + + // Scheduled pauses are recorded as a window that will later be added to all active deadlines. + if (locals.getAuctionPauseStateOutput.isPaused) + { + if (!state.get().isAuctionTimerPaused) + { + state.mut().isAuctionTimerPaused = 1; + state.mut().auctionTimerPauseStartedAt = locals.getAuctionPauseStateOutput.pauseStartedAt; + state.mut().auctionTimerPauseEndsAt = locals.getAuctionPauseStateOutput.pauseEndsAt; + return; + } + + if (!state.get().auctionTimerPauseStartedAt.isValid() || + locals.getAuctionPauseStateOutput.pauseStartedAt < state.get().auctionTimerPauseStartedAt) + { + state.mut().auctionTimerPauseStartedAt = locals.getAuctionPauseStateOutput.pauseStartedAt; + } + if (!state.get().auctionTimerPauseEndsAt.isValid() || locals.getAuctionPauseStateOutput.pauseEndsAt > state.get().auctionTimerPauseEndsAt) + { + state.mut().auctionTimerPauseEndsAt = locals.getAuctionPauseStateOutput.pauseEndsAt; + } + return; + } + + if (!state.get().isAuctionTimerPaused) + { + return; + } + + if (!state.get().auctionTimerPauseStartedAt.isValid() || !state.get().auctionTimerPauseEndsAt.isValid()) + { + state.mut().isAuctionTimerPaused = 0; + state.mut().auctionTimerPauseStartedAt.setInvalid(); + state.mut().auctionTimerPauseEndsAt.setInvalid(); + return; + } + + // When the pause ends, preserve elapsed auction time by extending every affected deadline. + diffDateInSecond(state.get().auctionTimerPauseStartedAt, state.get().auctionTimerPauseEndsAt, locals.pausedSeconds); + if (locals.pausedSeconds > 0) + { + locals.auctionIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); + while (locals.auctionIndex != NULL_INDEX) + { + locals.auction = state.get().auctionList.value(locals.auctionIndex); + if (locals.auction.core.status == EAuctionStatus::Active) + { + locals.auction.core.auctionDurationSeconds = sadd(locals.auction.core.auctionDurationSeconds, locals.pausedSeconds); + state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); + } + else if (locals.auction.core.status == EAuctionStatus::PendingSellerDecision && locals.auction.core.sellerDecisionDeadline.isValid()) + { + locals.auction.core.sellerDecisionDeadline.add(0, 0, 0, 0, 0, static_cast(locals.pausedSeconds)); + state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); + } + locals.auctionIndex = state.get().auctionList.nextElementIndex(locals.auctionIndex); + } + } + + state.mut().isAuctionTimerPaused = 0; + state.mut().auctionTimerPauseStartedAt.setInvalid(); + state.mut().auctionTimerPauseEndsAt.setInvalid(); + } + + /** + * @brief Returns remaining launch-delay ticks after the current epoch begins. + */ + PRIVATE_FUNCTION_WITH_LOCALS(GetTicksBeforeAuctionLaunchInternal) + { + output.ticks = 0; + + // An unarmed delay has no remaining ticks even if the current tick is near the epoch boundary. + if (!state.get().isPostBeginEpochPauseArmed) + { + return; + } + + output.ticks = static_cast(max(static_cast(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) - + (static_cast(qpi.tick()) - static_cast(qpi.initialTick())), + 0)); + } + + /** + * @brief Registers a QU obligation before the associated settlement becomes final. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(QueueQuPayout) + { + output.success = 0; + // Zero is an idempotent no-op, while a non-zero obligation must always have a payable recipient. + if (input.amount == 0 || isZero(input.recipient)) + { + output.success = input.amount == 0; + return; + } + + locals.previousAmount = 0; + // Reject aggregate overflow before touching either the per-wallet entry or its mirrored total. + if (input.amount > UINT64_MAX - state.get().totalPendingQuPayouts) + { + return; + } + + // Multiple settlements for one wallet share one liability entry to conserve bounded map capacity. + if (state.get().pendingQuPayouts.get(input.recipient, locals.previousAmount)) + { + // The wallet-level value must remain exactly reconcilable with totalPendingQuPayouts. + if (input.amount > UINT64_MAX - locals.previousAmount) + { + return; + } + + locals.updatedAmount = sadd(locals.previousAmount, input.amount); + if (!state.mut().pendingQuPayouts.replace(input.recipient, locals.updatedAmount)) + { + return; + } + } + else + { + // First-time recipients consume a new map slot; failure leaves the global liability total unchanged. + locals.payoutIndex = state.mut().pendingQuPayouts.set(input.recipient, input.amount); + if (locals.payoutIndex == NULL_INDEX) + { + return; + } + } + + state.mut().totalPendingQuPayouts = sadd(state.get().totalPendingQuPayouts, input.amount); + output.success = 1; + } + + /** + * @brief Pays a bounded number of chunks and preserves every unpaid remainder in state. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(FlushQuPayout) + { + output.success = 0; + output.transferredAmount = 0; + output.remainingAmount = 0; + // Absence is distinct from a paid zero balance because zero-balance entries are removed immediately. + if (!state.get().pendingQuPayouts.get(input.recipient, output.remainingAmount)) + { + return; + } + + locals.chunkIndex = 0; + // Bound both transfer size and iteration count so one payout attempt cannot exhaust contract execution time. + while (output.remainingAmount > 0 && locals.chunkIndex < input.maxChunks) + { + locals.chunkAmount = min(output.remainingAmount, static_cast(MAX_AMOUNT)); + locals.transferResult = qpi.transfer(input.recipient, static_cast(locals.chunkAmount)); + // A failed transfer stops delivery without decrementing the durable obligation. + if (locals.transferResult < 0) + { + break; + } + output.remainingAmount -= locals.chunkAmount; + output.transferredAmount = sadd(output.transferredAmount, locals.chunkAmount); + state.mut().totalPendingQuPayouts -= locals.chunkAmount; + ++locals.chunkIndex; + } + + // Fully discharged entries release map capacity; partial delivery persists the exact remainder for retry. + if (output.remainingAmount == 0) + { + state.mut().pendingQuPayouts.removeByKey(input.recipient); + } + else + { + state.mut().pendingQuPayouts.replace(input.recipient, output.remainingAmount); + } + output.success = 1; + } + + /** + * @brief Retries a bounded set of pending QU payouts and advances the persistent round-robin cursor. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(ProcessPendingQuPayouts) + { + locals.payoutScanIndex = mod(state.get().pendingPayoutScanCursor, state.get().pendingQuPayouts.capacity()); + locals.payoutTargetRecipientCount = min(state.get().pendingQuPayouts.population(), NOST_END_EPOCH_PAYOUT_RECIPIENT_NUM); + locals.processedPayoutRecipientCount = 0; + locals.payoutElementIndex = state.get().pendingQuPayouts.nextElementIndex(static_cast(locals.payoutScanIndex) - 1); + // Round-robin scanning bounds epoch work and prevents a permanently failing wallet from starving later map slots. + while (locals.processedPayoutRecipientCount < locals.payoutTargetRecipientCount) + { + // Wrap once the physical end is reached; the initial population snapshot prevents duplicate processing. + if (locals.payoutElementIndex == NULL_INDEX) + { + locals.payoutElementIndex = state.get().pendingQuPayouts.nextElementIndex(NULL_INDEX); + if (locals.payoutElementIndex == NULL_INDEX) + { + break; + } + } + locals.pendingPayoutRecipient = state.get().pendingQuPayouts.key(locals.payoutElementIndex); + locals.payoutScanIndex = mod(sadd(static_cast(locals.payoutElementIndex), 1ULL), state.get().pendingQuPayouts.capacity()); + locals.flushQuPayoutInput.recipient = locals.pendingPayoutRecipient; + locals.flushQuPayoutInput.maxChunks = NOST_END_EPOCH_PAYOUT_CHUNKS_PER_RECIPIENT; + CALL(FlushQuPayout, locals.flushQuPayoutInput, locals.flushQuPayoutOutput); + locals.processedPayoutRecipientCount = sadd(locals.processedPayoutRecipientCount, 1ULL); + locals.payoutElementIndex = state.get().pendingQuPayouts.nextElementIndex(locals.payoutElementIndex); + } + state.mut().pendingPayoutScanCursor = locals.payoutScanIndex; + } + + /** + * @brief Registers a payout exactly once for this call and immediately attempts bounded delivery. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(QueueAndFlushQuPayout) + { + output.success = 0; + output.transferredAmount = 0; + output.remainingAmount = 0; + locals.queueQuPayoutInput.recipient = input.recipient; + locals.queueQuPayoutInput.amount = input.amount; + CALL(QueueQuPayout, locals.queueQuPayoutInput, locals.queueQuPayoutOutput); + // Never attempt delivery unless the complete liability was made durable first. + if (!locals.queueQuPayoutOutput.success) + { + return; + } + // QueueQuPayout treats zero as success, but there is no map entry for FlushQuPayout to consume. + if (input.amount == 0) + { + output.success = 1; + return; + } + locals.flushQuPayoutInput.recipient = input.recipient; + locals.flushQuPayoutInput.maxChunks = input.maxChunks; + CALL(FlushQuPayout, locals.flushQuPayoutInput, locals.flushQuPayoutOutput); + output.transferredAmount = locals.flushQuPayoutOutput.transferredAmount; + output.remainingAmount = locals.flushQuPayoutOutput.remainingAmount; + output.success = locals.flushQuPayoutOutput.success; + } + + /** + * @brief Appends a participant snapshot to bounded history. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(ArchiveParticipant) + { + locals.historyIndex = mod(state.get().participantHistoryCounter, state.get().participantHistory.capacity()); + state.mut().participantHistory.set(locals.historyIndex, input.participantData); + state.mut().participantHistoryCounter = sadd(state.get().participantHistoryCounter, 1ULL); + } + + /** + * @brief Archives a closed auction and releases its active hash-map slot. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(ArchiveClosedAuction) + { + locals.historyIndex = mod(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); + state.mut().closedAuctionHistory.set(locals.historyIndex, input.auction); + state.mut().closedAuctionHistoryCounter = sadd(state.get().closedAuctionHistoryCounter, 1ULL); + state.mut().auctionList.removeByKey(input.auction.core.auctionIndex); + } + + /** + * @brief Finds a live auction or a retained closed-auction snapshot. + */ + PRIVATE_FUNCTION_WITH_LOCALS(FindAuction) + { + output.found = state.get().auctionList.get(input.auctionIndex, output.auction); + // Active storage is authoritative and avoids the bounded linear archive scan for live auctions. + if (output.found) + { + return; + } + // Closed auctions remain queryable only while their full snapshot is retained in the ring buffer. + for (locals.historyIndex = 0; locals.historyIndex < state.get().closedAuctionHistory.capacity(); ++locals.historyIndex) + { + locals.archivedAuction = state.get().closedAuctionHistory.get(locals.historyIndex); + if (locals.archivedAuction.core.status != EAuctionStatus::None && locals.archivedAuction.core.auctionIndex == input.auctionIndex) + { + output.auction = locals.archivedAuction; + output.found = 1; + return; + } + } + } + + /** + * @brief Tests retained closed history without copying an auction into the caller's locals. + */ + PRIVATE_FUNCTION_WITH_LOCALS(IsClosedAuctionRetained) + { + output.found = 0; + locals.retainedClosedAuctionCount = min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); + // Only initialized ring-buffer entries can match; inspect the const snapshot in place to keep this lookup lightweight. + for (locals.historyIndex = 0; locals.historyIndex < locals.retainedClosedAuctionCount; ++locals.historyIndex) + { + if (state.get().closedAuctionHistory.get(locals.historyIndex).core.status != EAuctionStatus::None && + state.get().closedAuctionHistory.get(locals.historyIndex).core.auctionIndex == input.auctionIndex) + { + output.found = 1; + return; + } + } + } + + /** + * @brief Selects the smallest retained auction index after an optional cursor. + */ + PRIVATE_FUNCTION_WITH_LOCALS(SelectNextRetainedAuction) + { + output.found = 0; + // Hash-map iteration is not creation ordered, so retain the smallest eligible live index beyond the cursor. + for (locals.auctionElementIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); locals.auctionElementIndex != NULL_INDEX; + locals.auctionElementIndex = state.get().auctionList.nextElementIndex(locals.auctionElementIndex)) + { + locals.candidateAuction = state.get().auctionList.value(locals.auctionElementIndex); + // Apply the cursor and optional seller filter before comparing creation indices. + if ((input.hasAfterAuctionIndex && locals.candidateAuction.core.auctionIndex <= input.afterAuctionIndex) || + (input.filterBySeller && locals.candidateAuction.core.seller != input.seller)) + { + continue; + } + if (!output.found || locals.candidateAuction.core.auctionIndex < output.auction.core.auctionIndex) + { + output.auction = locals.candidateAuction; + output.found = 1; + } + } + // Live-only callers avoid the archive scan entirely. + if (!input.includeClosedAuctions) + { + return; + } + + locals.retainedClosedAuctionCount = min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); + // Merge only retained closed snapshots without assuming physical ring order. + for (locals.historyIndex = 0; locals.historyIndex < locals.retainedClosedAuctionCount; ++locals.historyIndex) + { + locals.candidateAuction = state.get().closedAuctionHistory.get(locals.historyIndex); + // Apply the same cursor and seller filter to archived candidates. + if (locals.candidateAuction.core.status == EAuctionStatus::None || + (input.hasAfterAuctionIndex && locals.candidateAuction.core.auctionIndex <= input.afterAuctionIndex) || + (input.filterBySeller && locals.candidateAuction.core.seller != input.seller)) + { + continue; + } + if (!output.found || locals.candidateAuction.core.auctionIndex < output.auction.core.auctionIndex) + { + output.auction = locals.candidateAuction; + output.found = 1; + } + } + } + + /** + * @brief Counts retained live and closed auctions belonging to one seller without reconstructing creation order. + */ + PRIVATE_FUNCTION_WITH_LOCALS(CountRetainedAuctionsBySeller) + { + output.count = 0; + // A physical live-map pass is sufficient because counting does not depend on creation order. + for (locals.auctionElementIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); locals.auctionElementIndex != NULL_INDEX; + locals.auctionElementIndex = state.get().auctionList.nextElementIndex(locals.auctionElementIndex)) + { + locals.candidateAuction = state.get().auctionList.value(locals.auctionElementIndex); + if (locals.candidateAuction.core.seller == input.seller) + { + output.count = sadd(output.count, 1ULL); + } + } + + locals.retainedClosedAuctionCount = min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); + // Only initialized ring slots can contribute to the retained seller count. + for (locals.historyIndex = 0; locals.historyIndex < locals.retainedClosedAuctionCount; ++locals.historyIndex) + { + locals.candidateAuction = state.get().closedAuctionHistory.get(locals.historyIndex); + if (locals.candidateAuction.core.status != EAuctionStatus::None && locals.candidateAuction.core.seller == input.seller) + { + output.count = sadd(output.count, 1ULL); + } + } + } + + /** + * @brief Finds the smallest retained auction index whose complete fixed-size metadata CID matches the input. + */ + PRIVATE_FUNCTION_WITH_LOCALS(FindFirstRetainedAuctionByMetadataCid) + { + output.found = 0; + // Select the minimum matching live index directly instead of repeatedly reconstructing global order. + for (locals.auctionElementIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); locals.auctionElementIndex != NULL_INDEX; + locals.auctionElementIndex = state.get().auctionList.nextElementIndex(locals.auctionElementIndex)) + { + locals.candidateAuction = state.get().auctionList.value(locals.auctionElementIndex); + locals.metadataMatches = 1; + // Compare the complete fixed CID field, including zero padding. + for (locals.metadataIndex = 0; locals.metadataIndex < NOST_AUCTION_METADATA_CID_LENGTH; ++locals.metadataIndex) + { + if (locals.candidateAuction.core.metadataIpfsCid.get(locals.metadataIndex) != input.metadataIpfsCid.get(locals.metadataIndex)) + { + locals.metadataMatches = 0; + break; + } + } + if (locals.metadataMatches && (!output.found || locals.candidateAuction.core.auctionIndex < output.auction.core.auctionIndex)) + { + output.auction = locals.candidateAuction; + output.found = 1; + } + } + + locals.retainedClosedAuctionCount = min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); + // Closed snapshots share the same index ordering but occupy unordered ring slots. + for (locals.historyIndex = 0; locals.historyIndex < locals.retainedClosedAuctionCount; ++locals.historyIndex) + { + locals.candidateAuction = state.get().closedAuctionHistory.get(locals.historyIndex); + if (locals.candidateAuction.core.status == EAuctionStatus::None) + { + continue; + } + locals.metadataMatches = 1; + // Compare the complete fixed CID field, including zero padding. + for (locals.metadataIndex = 0; locals.metadataIndex < NOST_AUCTION_METADATA_CID_LENGTH; ++locals.metadataIndex) + { + if (locals.candidateAuction.core.metadataIpfsCid.get(locals.metadataIndex) != input.metadataIpfsCid.get(locals.metadataIndex)) + { + locals.metadataMatches = 0; + break; + } + } + if (locals.metadataMatches && (!output.found || locals.candidateAuction.core.auctionIndex < output.auction.core.auctionIndex)) + { + output.auction = locals.candidateAuction; + output.found = 1; + } + } + } + + /** + * @brief Pays auction sale proceeds to the seller and records every fee for end-of-epoch settlement. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(DistributeAuctionRevenue) + { + output.sellerPayout = input.grossAmount; + output.success = 0; + + // Zero-gross settlements still report success so callers can close no-sale auctions cleanly. + if (input.grossAmount == 0) + { + output.success = 1; + return; + } + // Only the seller is queued during settlement; fee recipients are handled by END_EPOCH. + if (state.get().pendingQuPayouts.population() > state.get().pendingQuPayouts.capacity() - NOST_AUCTION_REVENUE_MAX_PAYOUT_RECIPIENTS) + { + return; + } + + calculateAuctionRevenueBreakdown(input.grossAmount, state, locals.auctionRevenueBreakdown); + output.sellerPayout = locals.auctionRevenueBreakdown.sellerPayout; + + // Register the seller liability before recording fees so a queue-capacity failure cannot duplicate fee accrual on retry. + locals.payoutInput.recipient = input.seller; + locals.payoutInput.amount = output.sellerPayout; + locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } + + locals.feePool = state.get().feePool; + // The routing decision and fee configuration are captured when revenue is settled; recipient wallets are resolved at END_EPOCH. + if (routeAllFeesToDevelopment(state)) + { + locals.feePool.developmentAmount = sadd(locals.feePool.developmentAmount, input.grossAmount - output.sellerPayout); + } + else + { + locals.shareholderFeeTierIndex = getAuctionShareholderFeeTierIndex(input.grossAmount); + switch (locals.shareholderFeeTierIndex) + { + case 0: + locals.feePool.shareholderDividendTier1Amount = + sadd(locals.feePool.shareholderDividendTier1Amount, locals.auctionRevenueBreakdown.shareholderDividendAmount); + break; + case 1: + locals.feePool.shareholderDividendTier2Amount = + sadd(locals.feePool.shareholderDividendTier2Amount, locals.auctionRevenueBreakdown.shareholderDividendAmount); + break; + case 2: + locals.feePool.shareholderDividendTier3Amount = + sadd(locals.feePool.shareholderDividendTier3Amount, locals.auctionRevenueBreakdown.shareholderDividendAmount); + break; + default: + locals.feePool.shareholderDividendTier4Amount = + sadd(locals.feePool.shareholderDividendTier4Amount, locals.auctionRevenueBreakdown.shareholderDividendAmount); + break; + } + + locals.feePool.managementAmount = sadd(locals.feePool.managementAmount, locals.auctionRevenueBreakdown.managementFeeAmount); + locals.feePool.developmentAmount = sadd(locals.feePool.developmentAmount, locals.auctionRevenueBreakdown.developmentFeeAmount); + locals.feePool.takeoverCoordinatorAmount = + sadd(locals.feePool.takeoverCoordinatorAmount, locals.auctionRevenueBreakdown.takeoverCoordinatorFeeAmount); + } + + state.mut().feePool = locals.feePool; + output.success = 1; + } + + /** + * @brief Accumulates a service fee using the routing mode active when the fee is charged. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(AccumulateAuctionServiceFee) + { + output.success = 0; + + // Creation, bidding, and cancellation paths may call this with zero after fee configuration changes. + if (input.feeAmount == 0) + { + output.success = 1; + return; + } + + locals.feePool = state.get().feePool; + if (routeAllFeesToDevelopment(state)) + { + locals.feePool.developmentAmount = sadd(locals.feePool.developmentAmount, input.feeAmount); + } + else + { + locals.feePool.commonServiceFeeAmount = sadd(locals.feePool.commonServiceFeeAmount, input.feeAmount); + } + state.mut().feePool = locals.feePool; + output.success = 1; + } + + /** + * @brief Materializes compatible service fees and settles every shared pool accumulator using the recipients active at `END_EPOCH`. + * @note Each accumulator is cleared only after its value has moved to dividend dust or a durable payout liability. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(DistributeNostromoFeePool) + { + output.success = 0; + locals.feePool = state.get().feePool; + + if (locals.feePool.commonServiceFeeAmount > 0) + { + calculateAuctionServiceFeeBreakdown(locals.feePool.commonServiceFeeAmount, locals.auctionServiceFeeBreakdown); + locals.feePool.shareholderDividendAmount = + sadd(locals.feePool.shareholderDividendAmount, locals.auctionServiceFeeBreakdown.shareholderDividendAmount); + locals.feePool.managementAmount = sadd(locals.feePool.managementAmount, locals.auctionServiceFeeBreakdown.managementFeeAmount); + locals.feePool.developmentAmount = sadd(locals.feePool.developmentAmount, locals.auctionServiceFeeBreakdown.developmentFeeAmount); + locals.feePool.takeoverCoordinatorAmount = + sadd(locals.feePool.takeoverCoordinatorAmount, locals.auctionServiceFeeBreakdown.takeoverCoordinatorFeeAmount); + locals.feePool.commonServiceFeeAmount = 0; + state.mut().feePool = locals.feePool; + } + + locals.shareholderDividendAmount = + sadd(sadd(sadd(locals.feePool.shareholderDividendTier1Amount, locals.feePool.shareholderDividendTier2Amount), + sadd(locals.feePool.shareholderDividendTier3Amount, locals.feePool.shareholderDividendTier4Amount)), + locals.feePool.shareholderDividendAmount); + if (locals.shareholderDividendAmount > 0) + { + state.mut().auctionShareholderDividendPool = sadd(state.get().auctionShareholderDividendPool, locals.shareholderDividendAmount); + locals.feePool.shareholderDividendTier1Amount = 0; + locals.feePool.shareholderDividendTier2Amount = 0; + locals.feePool.shareholderDividendTier3Amount = 0; + locals.feePool.shareholderDividendTier4Amount = 0; + locals.feePool.shareholderDividendAmount = 0; + state.mut().feePool = locals.feePool; + } + + locals.dividendPerShare = div(state.get().auctionShareholderDividendPool, NUMBER_OF_COMPUTORS); + if (locals.dividendPerShare > 0 && qpi.distributeDividends(locals.dividendPerShare)) + { + locals.distributedDividendAmount = smul(locals.dividendPerShare, static_cast(NUMBER_OF_COMPUTORS)); + state.mut().auctionShareholderDividendPool -= locals.distributedDividendAmount; + } + + if (state.get().feePool.managementAmount > 0) + { + locals.payoutInput.recipient = state.get().management; + locals.payoutInput.amount = state.get().feePool.managementAmount; + locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } + state.mut().feePool.managementAmount = 0; + } + if (state.get().feePool.developmentAmount > 0) + { + locals.payoutInput.recipient = state.get().development; + locals.payoutInput.amount = state.get().feePool.developmentAmount; + locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } + state.mut().feePool.developmentAmount = 0; + } + if (state.get().feePool.takeoverCoordinatorAmount > 0) + { + locals.payoutInput.recipient = state.get().takeoverCoordinator; + locals.payoutInput.amount = state.get().feePool.takeoverCoordinatorAmount; + locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } + state.mut().feePool.takeoverCoordinatorAmount = 0; + } + + output.success = 1; + } + + /** + * @brief Counts non-empty wallet entries allowed to bid in a private auction. + */ + PRIVATE_FUNCTION_WITH_LOCALS(CountAllowedBidderWallets) + { + output.allowedWalletCount = 0; + for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < input.allowedBidderWallets.capacity(); ++locals.allowedWalletIndex) + { + if (!isZero(input.allowedBidderWallets.get(locals.allowedWalletIndex))) + { + output.allowedWalletCount = sadd(output.allowedWalletCount, 1ULL); + } + } + } + + /** + * @brief Counts valid access-asset requirements for private auction gating. + */ + PRIVATE_FUNCTION_WITH_LOCALS(CountRequiredAccessAssets) + { + output.requiredAccessAssetCount = 0; + output.isValid = 1; + // Empty asset slots are allowed only when their quantity is also empty. + for (locals.requiredAccessAssetIndex = 0; locals.requiredAccessAssetIndex < input.requiredAccessAssets.capacity(); + ++locals.requiredAccessAssetIndex) + { + locals.requiredAccessAsset = input.requiredAccessAssets.get(locals.requiredAccessAssetIndex); + if (isZeroAsset(locals.requiredAccessAsset.asset)) + { + if (locals.requiredAccessAsset.quantity != 0) + { + output.isValid = 0; + return; + } + continue; + } + + if (locals.requiredAccessAsset.quantity <= 0) + { + output.isValid = 0; + return; + } + + output.requiredAccessAssetCount = sadd(output.requiredAccessAssetCount, 1ULL); + } + } + + /** + * @brief Checks whether the invocator owns at least one configured access asset. + */ + PRIVATE_FUNCTION_WITH_LOCALS(HasRequiredAccessAsset) + { + output.hasRequiredAccessAsset = 0; + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + return; + } + + // Owning any one configured access asset at the required quantity grants private auction access. + for (locals.requiredAccessAssetSetIndex = locals.auction.requiredAccessAssets.nextElementIndex(NULL_INDEX); + locals.requiredAccessAssetSetIndex != NULL_INDEX; + locals.requiredAccessAssetSetIndex = locals.auction.requiredAccessAssets.nextElementIndex(locals.requiredAccessAssetSetIndex)) + { + locals.requiredAccessAsset.asset = locals.auction.requiredAccessAssets.key(locals.requiredAccessAssetSetIndex); + locals.requiredAccessAsset.quantity = locals.auction.requiredAccessAssets.value(locals.requiredAccessAssetSetIndex); + locals.possessedAccessShares = qpi.numberOfShares(locals.requiredAccessAsset.asset, AssetOwnershipSelect::byOwner(qpi.invocator()), + AssetPossessionSelect::byPossessor(qpi.invocator())); + if (locals.possessedAccessShares >= locals.requiredAccessAsset.quantity) + { + output.hasRequiredAccessAsset = 1; + return; + } + } + } + + /** + * @brief Recomputes the displayed highest active Batch Auction bid. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(RecomputeBatchHighestBid) + { + locals.bestParticipantFound = 0; + + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + return; + } + + if (locals.auction.core.type != EAuctionType::Batch) + { + return; + } + + // Batch auctions expose the highest active price, with FIFO tie-breaking for equal bids. + for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) + { + locals.participantData = state.get().participants.get(locals.participantIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) + { + continue; + } + + if (!locals.participantData.isActive || locals.participantData.escrowedAmount == 0) + { + continue; + } + + if (!locals.bestParticipantFound || locals.participantData.bidAmount > locals.bestParticipantData.bidAmount || + (locals.participantData.bidAmount == locals.bestParticipantData.bidAmount && + locals.participantData.bidIndex < locals.bestParticipantData.bidIndex)) + { + locals.bestParticipantFound = 1; + locals.bestParticipantData = locals.participantData; + locals.bestParticipantSlotIndex = locals.participantIndex; + } + } + + if (locals.bestParticipantFound) + { + locals.auction.core.highestBidder = locals.bestParticipantData.participant; + locals.auction.core.highestBidPrice = locals.bestParticipantData.bidAmount; + locals.auction.core.highestBidQuantity = locals.bestParticipantData.requestedQuantity; + locals.auction.core.highestBidAmount = locals.bestParticipantData.escrowedAmount; + locals.auction.core.highestBidSlotIndex = locals.bestParticipantSlotIndex; + } + else + { + locals.auction.core.highestBidAmount = 0; + locals.auction.core.highestBidPrice = 0; + locals.auction.core.highestBidQuantity = 0; + locals.auction.core.highestBidder = NULL_ID; + locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; + } + + state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); + } + + /** + * @brief Computes the price and quantity still available for a Batch Auction bid. + */ + PRIVATE_FUNCTION_WITH_LOCALS(ComputeBatchBidAvailability) + { + output.found = 0; + output.isAcceptingBids = 0; + output.minimumBidPrice = 0; + output.availableQuantity = 0; + locals.lowestWinningPriceFound = 0; + locals.lowestWinningPrice = 0; + locals.salePriorityQuantity = 0; + locals.priorityQuantity = 0; + + // Availability is defined only for a retained live auction; closed snapshots never accept bids. + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + return; + } + + output.found = 1; + if (locals.auction.core.type != EAuctionType::Batch || locals.auction.core.status != EAuctionStatus::Active || + locals.auction.core.quantityForSale < locals.auction.core.minimumPurchaseQuantity) + { + return; + } + + // Existing sale-price-or-better bids reserve priority quantity before a new bid can enter. + for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) + { + locals.participantData = state.get().participants.get(locals.participantIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) + { + continue; + } + + if (!locals.participantData.isActive || locals.participantData.escrowedAmount == 0 || locals.participantData.requestedQuantity == 0) + { + continue; + } + + if (!locals.lowestWinningPriceFound || locals.participantData.bidAmount < locals.lowestWinningPrice) + { + locals.lowestWinningPriceFound = 1; + locals.lowestWinningPrice = locals.participantData.bidAmount; + } + + if (locals.participantData.bidAmount >= locals.auction.core.salePrice) + { + locals.salePriorityQuantity = sadd(locals.salePriorityQuantity, locals.participantData.requestedQuantity); + } + } + + locals.effectiveCoverageQuantity = + locals.auction.core.quantityForSale - locals.auction.core.minimumPurchaseQuantity + NOST_BATCH_COVERAGE_THRESHOLD_OFFSET; + // Once less than one minimum allocation remains, report no sale-price capacity instead of an unusable fragment. + if (locals.salePriorityQuantity >= locals.effectiveCoverageQuantity) + { + output.availableQuantity = 0; + } + else + { + // Otherwise expose the full unreserved quantity; the minimum check below decides whether bidding remains viable. + output.availableQuantity = locals.auction.core.quantityForSale - locals.salePriorityQuantity; + } + + // If sale-price capacity is exhausted, new bids must improve the current lowest winning price. + if (output.availableQuantity >= locals.auction.core.minimumPurchaseQuantity) + { + output.minimumBidPrice = locals.auction.core.salePrice; + output.isAcceptingBids = 1; + } + else + { + // A full book can still accept a strictly better bid that displaces the current lowest-priced allocation. + output.availableQuantity = 0; + if (!locals.lowestWinningPriceFound || locals.lowestWinningPrice == UINT64_MAX) + { + return; + } + + output.minimumBidPrice = sadd(locals.lowestWinningPrice, 1ULL); + output.isAcceptingBids = 1; + if (input.bidAmount == 0) + { + return; + } + } + + locals.outputPrice = input.bidAmount > 0 ? input.bidAmount : output.minimumBidPrice; + if (locals.outputPrice < output.minimumBidPrice) + { + output.availableQuantity = 0; + return; + } + + // Recompute capacity at the requested price so callers know the maximum acceptable quantity. + locals.priorityQuantity = 0; + for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) + { + locals.participantData = state.get().participants.get(locals.participantIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) + { + continue; + } + + if (!locals.participantData.isActive || locals.participantData.escrowedAmount == 0 || locals.participantData.requestedQuantity == 0) + { + continue; + } + + if (locals.participantData.bidAmount > locals.outputPrice || locals.participantData.bidAmount == locals.outputPrice) + { + locals.priorityQuantity = sadd(locals.priorityQuantity, locals.participantData.requestedQuantity); + } + } + + // Equal-priced existing bids have FIFO priority, so a candidate at that price receives only later capacity. + if (locals.priorityQuantity >= locals.auction.core.quantityForSale) + { + output.availableQuantity = 0; + return; + } + + output.availableQuantity = locals.auction.core.quantityForSale - locals.priorityQuantity; + } + + /** + * @brief Validates, escrows, and ranks a new Batch Auction bid. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(ProcessBatchBid) + { + output.escrowedAmount = 0; + output.refundedAmount = 0; + output.errorCode = EAuctionError::Success; + output.success = 0; + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::AuctionNotFound; + return; + } + + if (input.effectiveQuantity < locals.auction.core.minimumPurchaseQuantity || input.bidAmount == 0) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::InvalidInput; + return; + } + // Retain enough payout slots to refund every active participant plus the caller's possible overpayment. + if (state.get().pendingQuPayouts.population() > + state.get().pendingQuPayouts.capacity() - state.get().participants.capacity() - NOST_BATCH_BID_CALLER_PAYOUT_RECIPIENTS) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::PayoutQueueFull; + return; + } + + calculateBatchAuctionBidFee(input.effectiveQuantity, input.bidAmount, locals.bidFeeCalculation); + if (locals.bidFeeCalculation.escrowAmount == 0) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::InvalidInput; + return; + } + + if (input.bidAmount < locals.auction.core.salePrice) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::BidTooLow; + return; + } + + locals.computeBatchBidAvailabilityInput.auctionIndex = input.auctionIndex; + locals.computeBatchBidAvailabilityInput.bidAmount = input.bidAmount; + CALL(ComputeBatchBidAvailability, locals.computeBatchBidAvailabilityInput, locals.computeBatchBidAvailabilityOutput); + if (!locals.computeBatchBidAvailabilityOutput.isAcceptingBids || input.bidAmount < locals.computeBatchBidAvailabilityOutput.minimumBidPrice) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::BidTooLow; + return; + } + if (input.effectiveQuantity > locals.computeBatchBidAvailabilityOutput.availableQuantity) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::QuantityUnavailable; + return; + } + + if (static_cast(qpi.invocationReward()) < locals.bidFeeCalculation.requiredReward) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::InsufficientFunds; + return; + } + + // Batch bids consume live slots only; displaced and settled records move to the history ring. + locals.freeParticipantSlotFound = 0; + for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) + { + locals.participantData = state.get().participants.get(locals.participantIndex); + if (!locals.participantData.isUsed) + { + locals.freeParticipantSlotFound = 1; + locals.freeParticipantSlotIndex = locals.participantIndex; + break; + } + } + + if (!locals.freeParticipantSlotFound || locals.auction.core.nextBidIndex == UINT64_MAX) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::StorageFull; + return; + } + + locals.participantData.escrowedAmount = locals.bidFeeCalculation.escrowAmount; + locals.participantData.requestedQuantity = input.effectiveQuantity; + locals.participantData.allocatedQuantity = 0; + locals.participantData.bidAmount = input.bidAmount; + locals.participantData.lastBidTime = input.currentDate; + locals.participantData.participant = qpi.invocator(); + locals.participantData.auctionIndex = input.auctionIndex; + locals.participantData.bidIndex = locals.auction.core.nextBidIndex; + locals.participantData.isUsed = 1; + locals.participantData.isActive = 1; + locals.participantData.isWinningBid = 1; + + // Accepted bids near deadline extend the auction to reduce last-moment sniping. + locals.auction.core.lastBidAt = input.currentDate; + if ((locals.auction.core.auctionDurationSeconds - input.elapsedSeconds) <= NOST_AUCTION_EXTENSION_SECONDS) + { + locals.auction.core.auctionDurationSeconds = sadd(locals.auction.core.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); + } + + locals.auction.core.nextBidIndex = sadd(locals.auction.core.nextBidIndex, 1ULL); + state.mut().participants.set(locals.freeParticipantSlotIndex, locals.participantData); + state.mut().auctionList.replace(input.auctionIndex, locals.auction); + + // Keep only the highest-priority quantity active; displaced escrow is refunded immediately. + locals.activeQuantity = 0; + for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) + { + locals.participantData = state.get().participants.get(locals.participantIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) + { + continue; + } + if (locals.participantData.isActive && locals.participantData.escrowedAmount > 0 && locals.participantData.requestedQuantity > 0) + { + locals.activeQuantity = sadd(locals.activeQuantity, locals.participantData.requestedQuantity); + } + } + + // Repeatedly evict the lowest-priority tail until active demand fits the finite lot supply. + while (locals.activeQuantity > locals.auction.core.quantityForSale) + { + locals.worstParticipantFound = 0; + // Lowest price loses first; for equal prices the newest bid loses to preserve FIFO priority. + for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) + { + locals.participantData = state.get().participants.get(locals.participantIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) + { + continue; + } + + if (!locals.participantData.isActive || locals.participantData.escrowedAmount == 0 || locals.participantData.requestedQuantity == 0) + { + continue; + } + + if (!locals.worstParticipantFound || locals.participantData.bidAmount < locals.worstParticipantData.bidAmount || + (locals.participantData.bidAmount == locals.worstParticipantData.bidAmount && + locals.participantData.bidIndex > locals.worstParticipantData.bidIndex)) + { + locals.worstParticipantFound = 1; + locals.worstParticipantData = locals.participantData; + locals.worstParticipantSlotIndex = locals.participantIndex; + } + } + + if (!locals.worstParticipantFound) + { + break; + } + + locals.excessQuantity = locals.activeQuantity - locals.auction.core.quantityForSale; + locals.displacedQuantity = min(locals.excessQuantity, locals.worstParticipantData.requestedQuantity); + locals.displacedRefund = smul(locals.displacedQuantity, locals.worstParticipantData.bidAmount); + locals.remainingWorstQuantity = locals.worstParticipantData.requestedQuantity - locals.displacedQuantity; + // A partial order smaller than the minimum is removed in full; keeping it would create an invalid final allocation. + if (locals.remainingWorstQuantity > 0 && locals.remainingWorstQuantity < locals.auction.core.minimumPurchaseQuantity) + { + locals.displacedQuantity = locals.worstParticipantData.requestedQuantity; + locals.displacedRefund = locals.worstParticipantData.escrowedAmount; + } + // Full displacement retires the live slot; partial displacement keeps a valid minimum-sized order active. + if (locals.displacedQuantity >= locals.worstParticipantData.requestedQuantity) + { + locals.worstParticipantData.escrowedAmount = 0; + locals.worstParticipantData.requestedQuantity = 0; + locals.worstParticipantData.allocatedQuantity = 0; + locals.worstParticipantData.isActive = 0; + locals.worstParticipantData.isWinningBid = 0; + } + else + { + locals.worstParticipantData.requestedQuantity -= locals.displacedQuantity; + locals.worstParticipantData.escrowedAmount -= locals.displacedRefund; + locals.worstParticipantData.isWinningBid = 1; + } + + if (locals.displacedRefund > 0) + { + locals.payoutInput.recipient = locals.worstParticipantData.participant; + locals.payoutInput.amount = locals.displacedRefund; + locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + output.refundedAmount = sadd(output.refundedAmount, locals.displacedRefund); + } + locals.activeQuantity -= locals.displacedQuantity; + // Archive only retired orders; partially displaced orders remain in the live priority book. + if (!locals.worstParticipantData.isActive) + { + locals.archiveParticipantInput.participantData = locals.worstParticipantData; + CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); + locals.worstParticipantData = {}; + } + state.mut().participants.set(locals.worstParticipantSlotIndex, locals.worstParticipantData); + } + + locals.recomputeBatchHighestBidInput.auctionIndex = input.auctionIndex; + CALL(RecomputeBatchHighestBid, locals.recomputeBatchHighestBidInput, locals.recomputeBatchHighestBidOutput); + + // Small-bid service fees are retained even if the bid is later displaced. + if (locals.bidFeeCalculation.fee > 0) + { + locals.accumulateAuctionServiceFeeInput.feeAmount = locals.bidFeeCalculation.fee; + CALL(AccumulateAuctionServiceFee, locals.accumulateAuctionServiceFeeInput, locals.accumulateAuctionServiceFeeOutput); + } + + if (static_cast(qpi.invocationReward()) > locals.bidFeeCalculation.requiredReward) + { + locals.payoutInput.recipient = qpi.invocator(); + locals.payoutInput.amount = static_cast(qpi.invocationReward()) - locals.bidFeeCalculation.requiredReward; + locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + output.refundedAmount = + sadd(output.refundedAmount, static_cast(qpi.invocationReward()) - locals.bidFeeCalculation.requiredReward); + } + + output.escrowedAmount = locals.bidFeeCalculation.escrowAmount; + output.success = 1; + } + + /** + * @brief Validates and records a Standard Auction bid, refunding replaced escrow. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(ProcessStandardBid) + { + output.escrowedAmount = 0; + output.refundedAmount = 0; + output.errorCode = EAuctionError::Success; + output.success = 0; + locals.highestBidderExists = 0; + locals.finalizeImmediately = 0; + locals.participantExists = 0; + locals.freeParticipantSlotFound = 0; + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + output.errorCode = EAuctionError::AuctionNotFound; + return; + } + + if (locals.auction.core.quantityForSale == 0 || locals.auction.core.quantityForSale < locals.auction.core.minimumPurchaseQuantity || + input.bidAmount == 0) + { + output.errorCode = EAuctionError::InvalidInput; + return; + } + // Reserve distinct entries for a replaced bidder, bidder change, three fee wallets, and the seller. + // This also guarantees that an accepted Buy Now bid can complete settlement in the same call. + if (state.get().pendingQuPayouts.population() > state.get().pendingQuPayouts.capacity() - NOST_STANDARD_BID_MAX_PAYOUT_RECIPIENTS) + { + output.errorCode = EAuctionError::PayoutQueueFull; + return; + } + + locals.requiredEscrow = input.bidAmount; + if (static_cast(qpi.invocationReward()) < locals.requiredEscrow) + { + output.errorCode = EAuctionError::InsufficientFunds; + return; + } + + if (locals.auction.core.highestBidPrice == 0) + { + if (input.bidAmount < locals.auction.core.initialPrice) + { + output.errorCode = EAuctionError::BidTooLow; + return; + } + } + else if (input.bidAmount < sadd(locals.auction.core.highestBidPrice, locals.auction.core.minimumBidIncrement)) + { + output.errorCode = EAuctionError::BidTooLow; + return; + } + + // Standard bidders update their own active slot, while a new bidder needs one reusable slot. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) + { + locals.participantData = state.get().participants.get(locals.participantSlotIndex); + if (locals.participantData.isUsed && locals.participantData.isActive && locals.participantData.auctionIndex == input.auctionIndex && + locals.participantData.participant == qpi.invocator()) + { + locals.participantExists = 1; + break; + } + if (!locals.freeParticipantSlotFound && !locals.participantData.isUsed) + { + locals.freeParticipantSlotFound = 1; + locals.freeParticipantSlotIndex = locals.participantSlotIndex; + } + } + locals.previousEscrow = locals.participantExists ? locals.participantData.escrowedAmount : 0; + if (!locals.participantExists && !locals.freeParticipantSlotFound) + { + output.errorCode = EAuctionError::StorageFull; + return; + } + if (!locals.participantExists) + { + locals.participantSlotIndex = locals.freeParticipantSlotIndex; + if (locals.auction.core.nextBidIndex == UINT64_MAX) + { + output.errorCode = EAuctionError::StorageFull; + return; + } + } + + locals.participantData.escrowedAmount = locals.requiredEscrow; + locals.participantData.requestedQuantity = locals.auction.core.quantityForSale; + locals.participantData.allocatedQuantity = 0; + locals.participantData.bidAmount = input.bidAmount; + locals.participantData.lastBidTime = input.currentDate; + locals.participantData.participant = qpi.invocator(); + locals.participantData.auctionIndex = input.auctionIndex; + locals.participantData.bidIndex = locals.participantExists ? locals.participantData.bidIndex : locals.auction.core.nextBidIndex; + locals.participantData.isUsed = 1; + locals.participantData.isActive = 1; + locals.participantData.isWinningBid = 0; + if (!locals.participantExists) + { + locals.auction.core.nextBidIndex = sadd(locals.auction.core.nextBidIndex, 1ULL); + } + + // A new highest bid releases the previous bidder's escrow before storing the replacement. + locals.highestBidderSlotIndex = locals.auction.core.highestBidSlotIndex; + if (locals.highestBidderSlotIndex < state.get().participants.capacity()) + { + locals.previousHighestBidderData = state.get().participants.get(locals.highestBidderSlotIndex); + locals.highestBidderExists = locals.previousHighestBidderData.isUsed && locals.previousHighestBidderData.isActive && + locals.previousHighestBidderData.auctionIndex == input.auctionIndex; + } + if (locals.highestBidderExists && locals.previousHighestBidderData.participant != qpi.invocator()) + { + locals.payoutInput.recipient = locals.previousHighestBidderData.participant; + locals.payoutInput.amount = locals.previousHighestBidderData.escrowedAmount; + locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + output.refundedAmount = sadd(output.refundedAmount, locals.previousHighestBidderData.escrowedAmount); + locals.previousHighestBidderData.escrowedAmount = 0; + locals.previousHighestBidderData.requestedQuantity = 0; + locals.previousHighestBidderData.isActive = 0; + locals.previousHighestBidderData.isWinningBid = 0; + locals.archiveParticipantInput.participantData = locals.previousHighestBidderData; + CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); + locals.previousHighestBidderData = {}; + state.mut().participants.set(locals.highestBidderSlotIndex, locals.previousHighestBidderData); + } + + locals.participantData.isWinningBid = 1; + locals.auction.core.highestBidder = qpi.invocator(); + locals.auction.core.highestBidPrice = input.bidAmount; + locals.auction.core.highestBidQuantity = locals.auction.core.quantityForSale; + locals.auction.core.highestBidAmount = locals.requiredEscrow; + locals.auction.core.highestBidSlotIndex = locals.participantSlotIndex; + + locals.auction.core.lastBidAt = input.currentDate; + if ((locals.auction.core.auctionDurationSeconds - input.elapsedSeconds) <= NOST_AUCTION_EXTENSION_SECONDS) + { + locals.auction.core.auctionDurationSeconds = sadd(locals.auction.core.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); + } + if (locals.auction.core.buyNowPrice > 0 && input.bidAmount >= locals.auction.core.buyNowPrice) + { + locals.finalizeImmediately = 1; + } + + state.mut().participants.set(locals.participantSlotIndex, locals.participantData); + state.mut().auctionList.replace(input.auctionIndex, locals.auction); + + // Refund replaced self-escrow and excess reward after the new bid state is durable. + if (locals.previousEscrow > 0) + { + locals.payoutInput.recipient = qpi.invocator(); + locals.payoutInput.amount = locals.previousEscrow; + locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + output.refundedAmount = sadd(output.refundedAmount, locals.previousEscrow); + } + if (static_cast(qpi.invocationReward()) > locals.requiredEscrow) + { + locals.payoutInput.recipient = qpi.invocator(); + locals.payoutInput.amount = static_cast(qpi.invocationReward()) - locals.requiredEscrow; + locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + output.refundedAmount = sadd(output.refundedAmount, static_cast(qpi.invocationReward()) - locals.requiredEscrow); + } + + output.escrowedAmount = locals.requiredEscrow; + output.success = 1; + + // Buy Now closes the auction in the same procedure after the winning bid is recorded. + if (locals.finalizeImmediately) + { + locals.finalizeStandardAuctionInput.auctionIndex = input.auctionIndex; + locals.finalizeStandardAuctionInput.currentDate = input.currentDate; + CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); + } + } + + /** + * @brief Validates the fixed-size IPFS metadata CID field. + */ + PRIVATE_FUNCTION_WITH_LOCALS(ValidateMetadataCid) + { + output.isValid = 0; + locals.hasPayloadCharacters = 0; + locals.reachedTerminator = 0; + + // Nostromo stores lowercase base32 CIDv1 values, which begin with the multibase prefix `b`. + if (input.metadataIpfsCid.get(0) != QPI::Ch::b) + { + return; + } + + // After the first zero byte, the fixed-size CID field must remain zero-padded. + for (locals.cidIndex = 1; locals.cidIndex < input.metadataIpfsCid.capacity(); ++locals.cidIndex) + { + locals.cidChar = input.metadataIpfsCid.get(locals.cidIndex); + if (locals.cidChar == 0) + { + locals.reachedTerminator = 1; + continue; + } + + if (locals.reachedTerminator) + { + return; + } + + if ((locals.cidChar >= QPI::Ch::a && locals.cidChar <= QPI::Ch::z) || (locals.cidChar >= QPI::Ch::_2 && locals.cidChar <= QPI::Ch::_7)) + { + locals.hasPayloadCharacters = 1; + continue; + } + + return; + } + + if (!locals.hasPayloadCharacters) + { + return; + } + + output.isValid = 1; + } + + /** + * @brief Verifies that the invocator can escrow every non-empty lot asset. + */ + PRIVATE_FUNCTION_WITH_LOCALS(VerifyAuctionLotBalances) + { + output.hasEnoughBalance = 1; + // Creation validates possession before attempting escrow so failures can refund without rollback. + for (locals.lotItemIndex = 0; locals.lotItemIndex < input.auctionLotItems.capacity(); ++locals.lotItemIndex) + { + locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); + if (isZeroAsset(locals.lotItem.asset) || locals.lotItem.quantity <= 0) + { + continue; + } + + locals.possessedShares = qpi.numberOfPossessedShares(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, qpi.invocator(), + qpi.invocator(), SELF_INDEX, SELF_INDEX); + if (locals.possessedShares < locals.lotItem.quantity) + { + output.hasEnoughBalance = 0; + return; + } + } + } + + /** + * @brief Returns escrowed lot assets to the specified recipient. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(RollbackAuctionLotAssets) + { + // Rollback is shared by cancellation, failed creation, rejected standard sales, and no-sale finalization. + for (locals.lotItemIndex = 0; locals.lotItemIndex < input.auctionLotItems.capacity(); ++locals.lotItemIndex) + { + locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); + if (isZeroAsset(locals.lotItem.asset) || locals.lotItem.quantity <= 0) + { + continue; + } + qpi.transferShareOwnershipAndPossession(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, SELF, SELF, locals.lotItem.quantity, + input.recipient); + } + } + + /** + * @brief Settles a Batch Auction by allocating winning quantities and closing the auction. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(FinalizeBatchAuction) + { + output.success = 0; + locals.bestParticipantFound = 0; + locals.lotItemFound = 0; + locals.soldQuantity = 0; + locals.totalGrossAmount = 0; + + // Abort if the auction no longer exists or is no longer an active batch auction. + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + return; + } + + if (locals.auction.core.type != EAuctionType::Batch || locals.auction.core.status != EAuctionStatus::Active) + { + return; + } + if (state.get().pendingQuPayouts.population() > + state.get().pendingQuPayouts.capacity() - state.get().participants.capacity() - NOST_AUCTION_REVENUE_MAX_PAYOUT_RECIPIENTS) + { + return; + } + + // Resolve the single sellable lot entry that represents the batch asset and quantity in escrow. + for (locals.lotItemIndex = 0; locals.lotItemIndex < locals.auction.core.auctionLotItems.capacity(); ++locals.lotItemIndex) + { + locals.batchLotItem = locals.auction.core.auctionLotItems.get(locals.lotItemIndex); + if (!isZeroAsset(locals.batchLotItem.asset) && locals.batchLotItem.quantity > 0) + { + locals.lotItemFound = 1; + break; + } + } + if (!locals.lotItemFound) + { + return; + } + + // Stop before producing a fragment below the auction minimum; the remainder stays with the seller. + locals.remainingQuantity = locals.auction.core.quantityForSale; + while (locals.remainingQuantity >= locals.auction.core.minimumPurchaseQuantity) + { + locals.bestParticipantFound = 0; + locals.participantIndex = 0; + + // Price priority is descending; the monotonic bid index is the only FIFO tie-breaker. + while (locals.participantIndex < state.get().participants.capacity()) + { + locals.participantData = state.get().participants.get(locals.participantIndex); + if (locals.participantData.isUsed && locals.participantData.auctionIndex == input.auctionIndex) + { + if (locals.participantData.isActive && locals.participantData.escrowedAmount > 0) + { + if (!locals.bestParticipantFound || locals.participantData.bidAmount > locals.bestParticipantData.bidAmount || + (locals.participantData.bidAmount == locals.bestParticipantData.bidAmount && + locals.participantData.bidIndex < locals.bestParticipantData.bidIndex)) + { + locals.bestParticipantFound = 1; + locals.bestParticipantData = locals.participantData; + locals.bestParticipantSlotIndex = locals.participantIndex; + } + } + } + ++locals.participantIndex; + } + + if (!locals.bestParticipantFound) + { + break; + } + + // Price the winning allocation and compute any escrow surplus that must be returned immediately. + locals.allocatedQuantity = min(locals.remainingQuantity, locals.bestParticipantData.requestedQuantity); + locals.requiredPayment = smul(locals.allocatedQuantity, locals.bestParticipantData.bidAmount); + locals.refundAmount = 0; + if (locals.bestParticipantData.escrowedAmount > locals.requiredPayment) + { + locals.refundAmount = locals.bestParticipantData.escrowedAmount - locals.requiredPayment; + } + + // Transfer the awarded shares, mark the participant as a winner, and advance settlement totals. + if (locals.allocatedQuantity > 0) + { + qpi.transferShareOwnershipAndPossession(locals.batchLotItem.asset.assetName, locals.batchLotItem.asset.issuer, SELF, SELF, + locals.allocatedQuantity, locals.bestParticipantData.participant); + locals.bestParticipantData.allocatedQuantity = locals.allocatedQuantity; + locals.bestParticipantData.isWinningBid = 1; + locals.soldQuantity = sadd(locals.soldQuantity, locals.allocatedQuantity); + locals.totalGrossAmount = sadd(locals.totalGrossAmount, locals.requiredPayment); + locals.remainingQuantity -= locals.allocatedQuantity; + } + + // Return the unused part of the winner escrow when the participant requested more than the remaining supply. + if (locals.refundAmount > 0) + { + locals.payoutInput.recipient = locals.bestParticipantData.participant; + locals.payoutInput.amount = locals.refundAmount; + locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } + } + + // Archive the completed bid and release its active slot immediately. + locals.bestParticipantData.escrowedAmount = 0; + locals.bestParticipantData.isActive = 0; + locals.archiveParticipantInput.participantData = locals.bestParticipantData; + CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); + locals.bestParticipantData = {}; + state.mut().participants.set(locals.bestParticipantSlotIndex, locals.bestParticipantData); + } + + // Refund every non-winning or non-allocated bid that still has escrow locked after winner selection. + locals.participantIndex = 0; + while (locals.participantIndex < state.get().participants.capacity()) + { + locals.participantData = state.get().participants.get(locals.participantIndex); + if (locals.participantData.isUsed && locals.participantData.auctionIndex == input.auctionIndex) + { + if (locals.participantData.escrowedAmount > 0) + { + locals.payoutInput.recipient = locals.participantData.participant; + locals.payoutInput.amount = locals.participantData.escrowedAmount; + locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } + locals.participantData.escrowedAmount = 0; + locals.participantData.allocatedQuantity = 0; + locals.participantData.isWinningBid = 0; + } + locals.participantData.isActive = 0; + locals.archiveParticipantInput.participantData = locals.participantData; + CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); + locals.participantData = {}; + state.mut().participants.set(locals.participantIndex, locals.participantData); + } + ++locals.participantIndex; + } + + // Return any unsold batch quantity to the seller when demand did not consume the entire lot. + if (locals.soldQuantity < locals.auction.core.quantityForSale) + { + qpi.transferShareOwnershipAndPossession(locals.batchLotItem.asset.assetName, locals.batchLotItem.asset.issuer, SELF, SELF, + locals.auction.core.quantityForSale - locals.soldQuantity, locals.auction.core.seller); + } + + // Split the collected proceeds according to Nostromo auction fee rules and pay the seller net amount. + locals.distributeAuctionRevenueInput.seller = locals.auction.core.seller; + locals.distributeAuctionRevenueInput.grossAmount = locals.totalGrossAmount; + CALL(DistributeAuctionRevenue, locals.distributeAuctionRevenueInput, locals.distributeAuctionRevenueOutput); + if (!locals.distributeAuctionRevenueOutput.success) + { + return; + } + + // Persist the final sold quantity and close the auction as settled. + locals.auction.core.allocatedQuantity = locals.soldQuantity; + locals.auction.core.status = EAuctionStatus::Finalized; + locals.auction.core.settledAt = input.currentDate; + locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; + state.mut().totalFinalizedAuctions = sadd(state.get().totalFinalizedAuctions, 1ULL); + locals.archiveClosedAuctionInput.auction = locals.auction; + CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); + output.success = 1; + } + + /** + * @brief Settles a Standard Auction by transferring the lot or returning it to the seller. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(FinalizeStandardAuction) + { + output.success = 0; + locals.highestBidderExists = 0; + locals.lotSold = 0; + + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + return; + } + + if (locals.auction.core.type != EAuctionType::Standard) + { + return; + } + if (state.get().pendingQuPayouts.population() > state.get().pendingQuPayouts.capacity() - NOST_STANDARD_FINALIZATION_MAX_PAYOUT_RECIPIENTS) + { + return; + } + + locals.highestBidderSlotIndex = locals.auction.core.highestBidSlotIndex; + if (locals.highestBidderSlotIndex < state.get().participants.capacity()) + { + locals.highestBidderData = state.get().participants.get(locals.highestBidderSlotIndex); + locals.highestBidderExists = + locals.highestBidderData.isUsed && locals.highestBidderData.isActive && locals.highestBidderData.auctionIndex == input.auctionIndex; + } + + // A valid highest bid transfers the whole standard lot and treats escrow as gross proceeds. + if (locals.highestBidderExists && locals.highestBidderData.escrowedAmount > 0) + { + locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = locals.highestBidderData.participant; + CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); + + locals.distributeAuctionRevenueInput.seller = locals.auction.core.seller; + locals.distributeAuctionRevenueInput.grossAmount = locals.highestBidderData.escrowedAmount; + CALL(DistributeAuctionRevenue, locals.distributeAuctionRevenueInput, locals.distributeAuctionRevenueOutput); + if (!locals.distributeAuctionRevenueOutput.success) + { + return; + } + + locals.highestBidderData.allocatedQuantity = locals.auction.core.quantityForSale; + locals.highestBidderData.isWinningBid = 1; + locals.highestBidderData.escrowedAmount = 0; + locals.highestBidderData.isActive = 0; + locals.archiveParticipantInput.participantData = locals.highestBidderData; + CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); + locals.highestBidderData = {}; + state.mut().participants.set(locals.highestBidderSlotIndex, locals.highestBidderData); + locals.auction.core.allocatedQuantity = locals.auction.core.quantityForSale; + locals.lotSold = 1; + } + else + { + // No active funded bid means the seller receives the lot back with no revenue distribution. + locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.core.seller; + CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); + locals.auction.core.allocatedQuantity = 0; + } + + // Closed standard auctions retain winner fields only when the lot actually sold. + locals.auction.core.status = EAuctionStatus::Finalized; + locals.auction.core.settledAt = input.currentDate; + if (!locals.lotSold) + { + locals.auction.core.highestBidAmount = 0; + locals.auction.core.highestBidPrice = 0; + locals.auction.core.highestBidQuantity = 0; + locals.auction.core.highestBidder = NULL_ID; + } + locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; + state.mut().totalFinalizedAuctions = sadd(state.get().totalFinalizedAuctions, 1ULL); + locals.archiveClosedAuctionInput.auction = locals.auction; + CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); + output.success = 1; + } + + /** + * @brief Rejects a pending Standard Auction bid and closes the auction without a sale. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(RejectStandardAuction) + { + output.refundedAmount = 0; + output.success = 0; + locals.highestBidderExists = 0; + + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + return; + } + + if (locals.auction.core.type != EAuctionType::Standard || locals.auction.core.status != EAuctionStatus::PendingSellerDecision) + { + return; + } + if (state.get().pendingQuPayouts.population() == state.get().pendingQuPayouts.capacity()) + { + return; + } + + locals.highestBidderSlotIndex = locals.auction.core.highestBidSlotIndex; + if (locals.highestBidderSlotIndex < state.get().participants.capacity()) + { + locals.highestBidderData = state.get().participants.get(locals.highestBidderSlotIndex); + locals.highestBidderExists = + locals.highestBidderData.isUsed && locals.highestBidderData.isActive && locals.highestBidderData.auctionIndex == input.auctionIndex; + } + + // Seller rejection unwinds the pending bid instead of distributing its escrow as proceeds. + if (locals.highestBidderExists && locals.highestBidderData.escrowedAmount > 0) + { + locals.payoutInput.recipient = locals.highestBidderData.participant; + locals.payoutInput.amount = locals.highestBidderData.escrowedAmount; + locals.payoutInput.maxChunks = NOST_IMMEDIATE_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } + output.refundedAmount = locals.highestBidderData.escrowedAmount; + locals.highestBidderData.escrowedAmount = 0; + locals.highestBidderData.allocatedQuantity = 0; + locals.highestBidderData.isActive = 0; + locals.highestBidderData.isWinningBid = 0; + locals.archiveParticipantInput.participantData = locals.highestBidderData; + CALL(ArchiveParticipant, locals.archiveParticipantInput, locals.archiveParticipantOutput); + locals.highestBidderData = {}; + state.mut().participants.set(locals.highestBidderSlotIndex, locals.highestBidderData); + } + + // The seller keeps the lot after rejection, and the auction is closed as finalized. + locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.core.seller; + CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); + + locals.auction.core.allocatedQuantity = 0; + locals.auction.core.highestBidAmount = 0; + locals.auction.core.highestBidPrice = 0; + locals.auction.core.highestBidQuantity = 0; + locals.auction.core.highestBidder = NULL_ID; + locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; + locals.auction.core.status = EAuctionStatus::Finalized; + locals.auction.core.settledAt = input.currentDate; + state.mut().totalFinalizedAuctions = sadd(state.get().totalFinalizedAuctions, 1ULL); + locals.archiveClosedAuctionInput.auction = locals.auction; + CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); + output.success = 1; + } + + /** + * @brief Transfers auction lot assets into contract escrow during creation. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(EscrowAuctionLotAssets) + { + output.success = 1; + // Escrow entries one by one; a later failure rolls back earlier successful transfers. + for (locals.lotItemIndex = 0; locals.lotItemIndex < input.auctionLotItems.capacity(); ++locals.lotItemIndex) + { + locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); + if (isZeroAsset(locals.lotItem.asset) || locals.lotItem.quantity <= 0) + { + continue; + } + + locals.remainingShares = qpi.transferShareOwnershipAndPossession(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, + qpi.invocator(), qpi.invocator(), locals.lotItem.quantity, SELF); + if (locals.remainingShares < 0) + { + // `transferShareOwnershipAndPossession` returns the remaining number of matching shares after a successful transfer. + // Negative values mean the transfer failed without moving the requested lot entry. + for (locals.rollbackLotItemIndex = 0; locals.rollbackLotItemIndex < locals.lotItemIndex; ++locals.rollbackLotItemIndex) + { + locals.lotItem = input.auctionLotItems.get(locals.rollbackLotItemIndex); + if (isZeroAsset(locals.lotItem.asset) || locals.lotItem.quantity <= 0) + { + continue; + } + qpi.transferShareOwnershipAndPossession(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, SELF, SELF, + locals.lotItem.quantity, qpi.invocator()); + } + output.success = 0; + return; + } + } + } + + /** + * @brief Creates a new Batch Auction or Standard Auction in the Nostromo Auction House. + * @note `CreateAuction_input` defines the IPFS metadata CID stored through Pinata, the auction lot, pricing, duration, and visibility rules. + * @note Batch auctions require `minimumPurchaseQuantity` in the range `[1, quantityForSale]`; standard auctions ignore it and store zero. + * @note A successful public Batch or Standard Auction accumulates the configured public creation fee, distributed at `END_EPOCH`. + * Insufficient payment rejects creation, overpayment is refunded, and failed creation refunds the full reward. + * @note Private auctions require the configured private auction fee, which is accumulated and distributed at `END_EPOCH` between shareholders + * and the configured fee recipients, and must use at least one access mode. If both modes are configured, either one grants access. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(CreateAuction) + { + output.errorCode = EAuctionError::InvalidInput; + + // Any rejection before escrow succeeds refunds the full invocation reward. + CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); + if (locals.isAuctionInteractionPausedOutput.isPaused) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::AuctionPaused; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + if (!isSupportedAuctionType(static_cast(input.auctionType))) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidAuctionType; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + if (!isSupportedAuctionVisibility(static_cast(input.auctionVisibility))) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidVisibility; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + if (state.get().auctionList.population() >= state.get().auctionList.capacity()) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::StorageFull; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + if (state.get().totalAuctionsCreated == UINT64_MAX) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::AuctionIndexExhausted; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + locals.validateMetadataCidInput.metadataIpfsCid = input.metadataIpfsCid; + CALL(ValidateMetadataCid, locals.validateMetadataCidInput, locals.validateMetadataCidOutput); + if (!locals.validateMetadataCidOutput.isValid) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + locals.analyzeAuctionLotInput.auctionLotItems = input.auctionLotItems; + locals.analyzeAuctionLotInput.durationDays = input.durationDays; + CALL(AnalyzeAuctionLot, locals.analyzeAuctionLotInput, locals.analyzeAuctionLotOutput); + if (!locals.analyzeAuctionLotOutput.isValid) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + // Resolve auction-type-specific quantity and price invariants before touching assets. + locals.resolvedQuantityForSale = 0; + locals.resolvedMinimumPurchaseQuantity = 0; + switch (static_cast(input.auctionType)) + { + case EAuctionType::Batch: + + if (!resolveBatchAuctionCreateParams(locals.analyzeAuctionLotOutput.lotItemCount, locals.analyzeAuctionLotOutput.totalEscrowQuantity, + input.minimumPurchaseQuantity, locals.resolvedQuantityForSale, + locals.resolvedMinimumPurchaseQuantity, input.buyNowPrice)) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + break; + case EAuctionType::Standard: + if (!resolveStandardAuctionCreateParams(input.minimumBidIncrement, locals.resolvedQuantityForSale, + locals.resolvedMinimumPurchaseQuantity, input.buyNowPrice, input.initialPrice, + input.salePrice)) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + break; + default: + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidAuctionType; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + // Private auctions require at least one access gate and may combine wallet and asset access. + locals.countAllowedBidderWalletsInput.allowedBidderWallets = input.allowedBidderWallets; + CALL(CountAllowedBidderWallets, locals.countAllowedBidderWalletsInput, locals.countAllowedBidderWalletsOutput); + locals.countRequiredAccessAssetsInput.requiredAccessAssets = input.requiredAccessAssets; + CALL(CountRequiredAccessAssets, locals.countRequiredAccessAssetsInput, locals.countRequiredAccessAssetsOutput); + if (!locals.countRequiredAccessAssetsOutput.isValid || + !validatePrivateAuctionAccess(static_cast(input.auctionVisibility), + locals.countRequiredAccessAssetsOutput.requiredAccessAssetCount, + locals.countAllowedBidderWalletsOutput.allowedWalletCount)) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + locals.requiredFee = getCreateAuctionFee(static_cast(input.auctionVisibility), state); + if (qpi.invocationReward() < locals.requiredFee) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - if (qpi.invocationReward() > NOSTROMO_CREATE_PROJECT_FEE) + output.errorCode = EAuctionError::InsufficientFunds; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + locals.verifyAuctionLotBalancesInput.auctionLotItems = input.auctionLotItems; + CALL(VerifyAuctionLotBalances, locals.verifyAuctionLotBalancesInput, locals.verifyAuctionLotBalancesOutput); + if (!locals.verifyAuctionLotBalancesOutput.hasEnoughBalance) + { + if (qpi.invocationReward() > 0) { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - NOSTROMO_CREATE_PROJECT_FEE); + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - state.mut().epochRevenue += NOSTROMO_CREATE_PROJECT_FEE; + output.errorCode = EAuctionError::InsufficientAssetBalance; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } - locals.newProject.creator = qpi.invocator(); - locals.newProject.tokenName = input.tokenName; - locals.newProject.supplyOfToken = input.supply; - locals.newProject.startDate = locals.startDate; - locals.newProject.endDate = locals.endDate; - locals.newProject.numberOfYes = 0; - locals.newProject.numberOfNo = 0; + // From this point onward, asset escrow may need explicit rollback on storage failure. + locals.escrowAuctionLotAssetsInput.auctionLotItems = input.auctionLotItems; + CALL(EscrowAuctionLotAssets, locals.escrowAuctionLotAssetsInput, locals.escrowAuctionLotAssetsOutput); + if (!locals.escrowAuctionLotAssetsOutput.success) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InsufficientAssetBalance; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } - output.indexOfProject = state.get().numberOfCreatedProject; - state.mut().projects.set(state.get().numberOfCreatedProject, locals.newProject); - state.mut().numberOfCreatedProject++; - state.mut().tokens.add(input.tokenName); + locals.auction.core.auctionIndex = state.get().totalAuctionsCreated; + locals.auction.core.quantityForSale = locals.resolvedQuantityForSale; + locals.auction.core.minimumPurchaseQuantity = locals.resolvedMinimumPurchaseQuantity; + locals.auction.core.initialPrice = input.initialPrice; + locals.auction.core.salePrice = input.salePrice; + locals.auction.core.minimumBidIncrement = input.minimumBidIncrement; + locals.auction.core.buyNowPrice = input.buyNowPrice; + locals.auction.core.auctionDurationSeconds = smul(static_cast(input.durationDays), NOST_SECONDS_PER_DAY); + locals.auction.core.createdAt = qpi.now(); + locals.auction.core.lastBidAt = locals.auction.core.createdAt; + locals.auction.core.seller = qpi.invocator(); + locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; + // Duplicate required access assets collapse to the highest configured quantity. + for (locals.requiredAccessAssetIndex = 0; locals.requiredAccessAssetIndex < input.requiredAccessAssets.capacity(); + ++locals.requiredAccessAssetIndex) + { + locals.requiredAccessAsset = input.requiredAccessAssets.get(locals.requiredAccessAssetIndex); + if (!isZeroAsset(locals.requiredAccessAsset.asset) && + (!locals.auction.requiredAccessAssets.get(locals.requiredAccessAsset.asset, locals.existingRequiredAccessQuantity) || + locals.requiredAccessAsset.quantity > locals.existingRequiredAccessQuantity)) + { + locals.auction.requiredAccessAssets.set(locals.requiredAccessAsset.asset, locals.requiredAccessAsset.quantity); + } } - else + locals.auction.core.auctionLotItems = input.auctionLotItems; + for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < input.allowedBidderWallets.capacity(); ++locals.allowedWalletIndex) + { + if (!isZero(input.allowedBidderWallets.get(locals.allowedWalletIndex))) + { + locals.auction.allowedBidderWallets.add(input.allowedBidderWallets.get(locals.allowedWalletIndex)); + } + } + locals.auction.core.metadataIpfsCid = input.metadataIpfsCid; + locals.auction.core.type = static_cast(input.auctionType); + locals.auction.core.visibility = static_cast(input.auctionVisibility); + locals.auction.core.status = EAuctionStatus::Active; + + // If persistent auction storage fails after escrow, return the lot before refunding the fee reward. + if (state.mut().auctionList.set(locals.auction.core.auctionIndex, locals.auction) == NULL_INDEX) { + locals.rollbackAuctionLotAssetsInput.auctionLotItems = input.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = qpi.invocator(); + CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.indexOfProject = NOSTROMO_MAX_NUMBER_PROJECT; + output.errorCode = EAuctionError::StorageFull; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); + logProcedureResult(locals.log); + return; + } + + // Creation fees are held until END_EPOCH; overpayment is returned immediately. + if (locals.requiredFee > 0) + { + locals.accumulateAuctionServiceFeeInput.feeAmount = static_cast(locals.requiredFee); + CALL(AccumulateAuctionServiceFee, locals.accumulateAuctionServiceFeeInput, locals.accumulateAuctionServiceFeeOutput); + } + + if (qpi.invocationReward() > locals.requiredFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.requiredFee); } + + output.auctionIndex = locals.auction.core.auctionIndex; + state.mut().totalAuctionsCreated = sadd(state.get().totalAuctionsCreated, 1ULL); + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, qpi.invocationReward()); + logProcedureResult(locals.log); } - struct voteInProject_locals + /** + * @brief Places a bid in an active auction. + * @note Batch auctions interpret `bidAmount` as price per asset and reject requested `quantity` below `minimumPurchaseQuantity` with a full + * refund. + * @note An accepted Batch bid escrows `quantity * bidAmount` and accumulates `max(100 - quantity * bidAmount, 0)` qu for distribution at + * `END_EPOCH`. Excess reward is refunded; rejected bids refund the full reward. The accumulated fee is not refunded if the bid is later + * displaced. + * @note Batch final allocations are also at least `minimumPurchaseQuantity`; smaller unsold remainders return to the seller and affected bids are + * fully refunded. + * @note Standard auctions interpret `bidAmount` as the total price for the whole lot and ignore `quantity`. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(PlaceBid) { - projectInfo votedProject; - Array votedList; - uint32 elementIndex, curDate, numberOfVotedProject, i; - bit flag; - }; + output.errorCode = EAuctionError::InvalidInput; - PUBLIC_PROCEDURE_WITH_LOCALS(voteInProject) - { - if (input.indexOfProject >= state.get().numberOfCreatedProject) + // Common auction gates run before type-specific bid processing; failed gates refund the reward. + CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); + if (locals.isAuctionInteractionPausedOutput.isPaused) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::AuctionPaused; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); + logProcedureResult(locals.log); + return; + } + + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { - return ; + locals.findAuctionInput.auctionIndex = input.auctionIndex; + CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = locals.findAuctionOutput.found ? EAuctionError::AuctionClosed : EAuctionError::AuctionNotFound; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); + logProcedureResult(locals.log); + return; } - if (state.get().users.contains(qpi.invocator()) == 0) + + if (locals.auction.core.status != EAuctionStatus::Active) { - return ; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::AuctionClosed; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); + logProcedureResult(locals.log); + return; } - state.get().numberOfVotedProject.get(qpi.invocator(), locals.numberOfVotedProject); - if (locals.numberOfVotedProject == NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST) + + if (locals.auction.core.seller == qpi.invocator()) { - return ; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); + logProcedureResult(locals.log); + return; } - state.get().voteStatus.get(qpi.invocator(), locals.votedList); - for (locals.i = 0; locals.i < locals.numberOfVotedProject; locals.i++) + + locals.currentDate = qpi.now(); + diffDateInSecond(locals.auction.core.createdAt, locals.currentDate, locals.elapsedSeconds); + if (locals.elapsedSeconds >= locals.auction.core.auctionDurationSeconds) { - if (locals.votedList.get(locals.i) == input.indexOfProject) + if (qpi.invocationReward() > 0) { - return ; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + output.errorCode = EAuctionError::AuctionClosed; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); + logProcedureResult(locals.log); + return; } - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); - if (locals.curDate >= state.get().projects.get(input.indexOfProject).startDate && locals.curDate < state.get().projects.get(input.indexOfProject).endDate) + + // When both gates are configured, satisfying either one grants access. + if (locals.auction.core.visibility == EAuctionVisibility::Private) { - locals.votedProject = state.get().projects.get(input.indexOfProject); - if (input.decision) + locals.hasAccess = locals.auction.allowedBidderWallets.population() > 0 && locals.auction.allowedBidderWallets.contains(qpi.invocator()); + if (!locals.hasAccess && locals.auction.requiredAccessAssets.population() > 0) { - locals.votedProject.numberOfYes++; + locals.hasRequiredAccessAssetInput.auctionIndex = input.auctionIndex; + CALL(HasRequiredAccessAsset, locals.hasRequiredAccessAssetInput, locals.hasRequiredAccessAssetOutput); + locals.hasAccess = locals.hasRequiredAccessAssetOutput.hasRequiredAccessAsset; } - else + + if (!locals.hasAccess) { - locals.votedProject.numberOfNo++; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::PrivateAuctionAccessDenied; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, + output.escrowedAmount); + logProcedureResult(locals.log); + return; } - state.mut().projects.set(input.indexOfProject, locals.votedProject); - locals.votedList.set(locals.numberOfVotedProject++, input.indexOfProject); - state.mut().voteStatus.set(qpi.invocator(), locals.votedList); - state.mut().numberOfVotedProject.set(qpi.invocator(), locals.numberOfVotedProject); } - } - struct createFundraising_locals - { - projectInfo tmpProject; - fundaraisingInfo newFundraising; - uint32 curDate, firstPhaseStartDate, firstPhaseEndDate, secondPhaseStartDate, secondPhaseEndDate, thirdPhaseStartDate, thirdPhaseEndDate, listingStartDate, cliffEndDate, vestingEndDate; - }; + // Type-specific processors own escrow/refund details once common validation succeeds. + switch (locals.auction.core.type) + { + case EAuctionType::Batch: + locals.processBatchBidInput.auctionIndex = input.auctionIndex; + locals.processBatchBidInput.effectiveQuantity = input.quantity; + locals.processBatchBidInput.bidAmount = input.bidAmount; + locals.processBatchBidInput.currentDate = locals.currentDate; + locals.processBatchBidInput.elapsedSeconds = locals.elapsedSeconds; + CALL(ProcessBatchBid, locals.processBatchBidInput, locals.processBatchBidOutput); + if (!locals.processBatchBidOutput.success) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.refundedAmount = locals.processBatchBidOutput.refundedAmount; + output.errorCode = locals.processBatchBidOutput.errorCode; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, + output.escrowedAmount); + logProcedureResult(locals.log); + return; + } + output.refundedAmount = sadd(output.refundedAmount, locals.processBatchBidOutput.refundedAmount); + output.escrowedAmount = locals.processBatchBidOutput.escrowedAmount; + break; + case EAuctionType::Standard: + locals.processStandardBidInput.auctionIndex = input.auctionIndex; + locals.processStandardBidInput.bidAmount = input.bidAmount; + locals.processStandardBidInput.currentDate = locals.currentDate; + locals.processStandardBidInput.elapsedSeconds = locals.elapsedSeconds; + CALL(ProcessStandardBid, locals.processStandardBidInput, locals.processStandardBidOutput); + if (!locals.processStandardBidOutput.success) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = locals.processStandardBidOutput.errorCode; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, + output.escrowedAmount); + logProcedureResult(locals.log); + return; + } + output.refundedAmount = sadd(output.refundedAmount, locals.processStandardBidOutput.refundedAmount); + output.escrowedAmount = locals.processStandardBidOutput.escrowedAmount; + break; + default: + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::InvalidAuctionType; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, + output.escrowedAmount); + logProcedureResult(locals.log); + return; + } + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); + logProcedureResult(locals.log); + } - PUBLIC_PROCEDURE_WITH_LOCALS(createFundraising) + /** + * @brief Cancels an active auction before the first accepted bid is placed. + * @note Once any bid is accepted, the seller can no longer cancel the auction. + * @note The cancellation fee is based on the configured reserve price for the full batch quantity or standard lot and is distributed between + * shareholders and the configured fee recipients. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(CancelAuction) { - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); - packNostromoDate(input.firstPhaseStartYear, input.firstPhaseStartMonth, input.firstPhaseStartDay, input.firstPhaseStartHour, 0, 0, locals.firstPhaseStartDate); - packNostromoDate(input.secondPhaseStartYear, input.secondPhaseStartMonth, input.secondPhaseStartDay, input.secondPhaseStartHour, 0, 0, locals.secondPhaseStartDate); - packNostromoDate(input.thirdPhaseStartYear, input.thirdPhaseStartMonth, input.thirdPhaseStartDay, input.thirdPhaseStartHour, 0, 0, locals.thirdPhaseStartDate); - packNostromoDate(input.firstPhaseEndYear, input.firstPhaseEndMonth, input.firstPhaseEndDay, input.firstPhaseEndHour, 0, 0, locals.firstPhaseEndDate); - packNostromoDate(input.secondPhaseEndYear, input.secondPhaseEndMonth, input.secondPhaseEndDay, input.secondPhaseEndHour, 0, 0, locals.secondPhaseEndDate); - packNostromoDate(input.thirdPhaseEndYear, input.thirdPhaseEndMonth, input.thirdPhaseEndDay, input.thirdPhaseEndHour, 0, 0, locals.thirdPhaseEndDate); - packNostromoDate(input.listingStartYear, input.listingStartMonth, input.listingStartDay, input.listingStartHour, 0, 0, locals.listingStartDate); - packNostromoDate(input.cliffEndYear, input.cliffEndMonth, input.cliffEndDay, input.cliffEndHour, 0, 0, locals.cliffEndDate); - packNostromoDate(input.vestingEndYear, input.vestingEndMonth, input.vestingEndDay, input.vestingEndHour, 0, 0, locals.vestingEndDate); + output.refundedAmount = 0; + output.cancellationFee = 0; + output.errorCode = EAuctionError::InvalidInput; - if (locals.curDate > locals.firstPhaseStartDate || locals.firstPhaseStartDate >= locals.firstPhaseEndDate || locals.firstPhaseEndDate > locals.secondPhaseStartDate || locals.secondPhaseStartDate >= locals.secondPhaseEndDate || locals.secondPhaseEndDate > locals.thirdPhaseStartDate || locals.thirdPhaseStartDate >= locals.thirdPhaseEndDate || locals.thirdPhaseEndDate > locals.listingStartDate || locals.listingStartDate > locals.cliffEndDate || locals.cliffEndDate > locals.vestingEndDate) + // Cancellation is blocked during emergency pause but does not use the scheduled auction timer pause. + if (state.get().isEmergencyPaused) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = EAuctionError::AuctionPaused; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); + logProcedureResult(locals.log); + return; } - if (checkValidNostDateTime(locals.firstPhaseStartDate) == 0 || checkValidNostDateTime(locals.firstPhaseEndDate) == 0 || checkValidNostDateTime(locals.secondPhaseStartDate) == 0 || checkValidNostDateTime(locals.secondPhaseEndDate) == 0 || checkValidNostDateTime(locals.thirdPhaseStartDate) == 0 || checkValidNostDateTime(locals.thirdPhaseEndDate) == 0 || checkValidNostDateTime(locals.listingStartDate) == 0 || checkValidNostDateTime(locals.cliffEndDate) == 0 || checkValidNostDateTime(locals.vestingEndDate) == 0) + + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { + locals.findAuctionInput.auctionIndex = input.auctionIndex; + CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = locals.findAuctionOutput.found ? EAuctionError::AuctionClosed : EAuctionError::AuctionNotFound; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); + logProcedureResult(locals.log); + return; } - if (input.stepOfVesting == 0 || input.stepOfVesting > 12 || input.TGE > 50 || input.threshold > 50 || input.indexOfProject >= state.get().numberOfCreatedProject) + if (locals.auction.core.status != EAuctionStatus::Active) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = EAuctionError::AuctionClosed; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); + logProcedureResult(locals.log); + return; } - - if (state.get().projects.get(input.indexOfProject).creator != qpi.invocator()) + if (locals.auction.core.seller != qpi.invocator()) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); + logProcedureResult(locals.log); + return; } - if (input.soldAmount > state.get().projects.get(input.indexOfProject).supplyOfToken) + if (locals.auction.core.nextBidIndex != 0) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = EAuctionError::AuctionHasAcceptedBid; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); + logProcedureResult(locals.log); + return; } - if (locals.curDate <= state.get().projects.get(input.indexOfProject).endDate || state.get().projects.get(input.indexOfProject).numberOfYes <= state.get().projects.get(input.indexOfProject).numberOfNo || state.get().projects.get(input.indexOfProject).isCreatedFundarasing == 1) + // The fee base represents the full reserve value of the lot being withdrawn. + locals.cancellationBaseAmount = locals.auction.core.salePrice; + if (locals.auction.core.type == EAuctionType::Batch) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; + locals.cancellationBaseAmount = smul(locals.auction.core.salePrice, locals.auction.core.quantityForSale); } + output.cancellationFee = calculateBasisPointAmount(locals.cancellationBaseAmount, state.get().auctionCancellationFeeBasisPoints); - if (input.tokenPrice * input.soldAmount < input.requiredFunds + div(input.requiredFunds * input.threshold, 100ULL)) + if (static_cast(qpi.invocationReward()) < output.cancellationFee) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = EAuctionError::InsufficientFunds; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); + logProcedureResult(locals.log); + return; + } + + locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.core.seller; + CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); + + // Cancellation closes the auction and records it in the same history ring as finalized auctions. + locals.currentDate = qpi.now(); + locals.auction.core.status = EAuctionStatus::Cancelled; + locals.auction.core.settledAt = locals.currentDate; + locals.auction.core.allocatedQuantity = 0; + locals.auction.core.highestBidAmount = 0; + locals.auction.core.highestBidPrice = 0; + locals.auction.core.highestBidQuantity = 0; + locals.auction.core.highestBidder = NULL_ID; + locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; + state.mut().totalCancelledAuctions = sadd(state.get().totalCancelledAuctions, 1ULL); + locals.archiveClosedAuctionInput.auction = locals.auction; + CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); + + // Cancellation fees use the same epoch pool as creation and small-bid service fees. + locals.accumulateAuctionServiceFeeInput.feeAmount = output.cancellationFee; + CALL(AccumulateAuctionServiceFee, locals.accumulateAuctionServiceFeeInput, locals.accumulateAuctionServiceFeeOutput); + + if (static_cast(qpi.invocationReward()) > output.cancellationFee) + { + qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - output.cancellationFee); + } + + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, output.cancellationFee); + logProcedureResult(locals.log); + } + + /** + * @brief Lets the seller accept or reject a pending standard auction whose highest bid stayed below the sale price. + * @note The manual decision window lasts one week; after expiry the contract finalizes the sale automatically in favor of the buyer. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(ResolvePendingStandardAuction) + { + output.refundedAmount = 0; + output.errorCode = EAuctionError::InvalidInput; + + // This procedure does not need a reward; return any supplied amount before validation. + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); + if (locals.isAuctionInteractionPausedOutput.isPaused) + { + output.errorCode = EAuctionError::AuctionPaused; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); + logProcedureResult(locals.log); + return; + } + + if (input.acceptSale > 1) + { + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); + logProcedureResult(locals.log); + return; + } + + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + locals.findAuctionInput.auctionIndex = input.auctionIndex; + CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); + output.errorCode = locals.findAuctionOutput.found ? EAuctionError::AuctionClosed : EAuctionError::AuctionNotFound; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); + logProcedureResult(locals.log); + return; + } + + if (locals.auction.core.seller != qpi.invocator()) + { + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); + logProcedureResult(locals.log); + return; + } + + if (locals.auction.core.type != EAuctionType::Standard || locals.auction.core.status != EAuctionStatus::PendingSellerDecision) + { + output.errorCode = EAuctionError::AuctionClosed; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); + logProcedureResult(locals.log); + return; + } + + // If the decision window has expired, the automatic sale wins over the seller action. + locals.currentDate = qpi.now(); + if (!state.get().isAuctionTimerPaused && locals.auction.core.sellerDecisionDeadline <= locals.currentDate) + { + locals.finalizeStandardAuctionInput.auctionIndex = input.auctionIndex; + locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); + + output.errorCode = EAuctionError::AuctionClosed; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); + logProcedureResult(locals.log); + return; + } + + // Accepting finalizes the sale; rejecting refunds the bidder and returns the lot to the seller. + if (input.acceptSale) + { + locals.finalizeStandardAuctionInput.auctionIndex = input.auctionIndex; + locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); + output.errorCode = locals.finalizeStandardAuctionOutput.success ? EAuctionError::Success : EAuctionError::AuctionClosed; + } + else + { + locals.rejectStandardAuctionInput.auctionIndex = input.auctionIndex; + locals.rejectStandardAuctionInput.currentDate = locals.currentDate; + CALL(RejectStandardAuction, locals.rejectStandardAuctionInput, locals.rejectStandardAuctionOutput); + output.refundedAmount = locals.rejectStandardAuctionOutput.refundedAmount; + output.errorCode = locals.rejectStandardAuctionOutput.success ? EAuctionError::Success : EAuctionError::AuctionClosed; + } + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); + logProcedureResult(locals.log); + } + + /** + * @brief Overwrites the full auction fee configuration. + * @note Only the configured takeover coordinator can call this procedure. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(SetAuctionFees) + { + output.errorCode = EAuctionError::InvalidInput; + // Administrative procedures never consume invocation rewards. + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.get().takeoverCoordinator) + { + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFees, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + // Validate all fee tiers together so no gross-proceeds tier can exceed 100 percent. + if (!isValidAuctionFeeConfiguration(input.privateAuctionFee, input.publicAuctionCreationFee, input.auctionCancellationFeeBasisPoints, + input.managementFeeBasisPoints, input.developmentFeeBasisPoints, input.takeoverCoordinatorFeeBasisPoints, + input.shareholderDividendBasisPoints, input.shareholderFeeBasisPointsTier1, + input.shareholderFeeBasisPointsTier2, input.shareholderFeeBasisPointsTier3, + input.shareholderFeeBasisPointsTier4)) + { + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFees, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + state.mut().privateAuctionFee = input.privateAuctionFee; + state.mut().publicAuctionCreationFee = input.publicAuctionCreationFee; + state.mut().auctionCancellationFeeBasisPoints = input.auctionCancellationFeeBasisPoints; + state.mut().managementFeeBasisPoints = input.managementFeeBasisPoints; + state.mut().developmentFeeBasisPoints = input.developmentFeeBasisPoints; + state.mut().takeoverCoordinatorFeeBasisPoints = input.takeoverCoordinatorFeeBasisPoints; + state.mut().shareholderDividendBasisPoints = input.shareholderDividendBasisPoints; + state.mut().shareholderFeeBasisPointsTier1 = input.shareholderFeeBasisPointsTier1; + state.mut().shareholderFeeBasisPointsTier2 = input.shareholderFeeBasisPointsTier2; + state.mut().shareholderFeeBasisPointsTier3 = input.shareholderFeeBasisPointsTier3; + state.mut().shareholderFeeBasisPointsTier4 = input.shareholderFeeBasisPointsTier4; + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFees, output.errorCode, 0, 0); + logProcedureResult(locals.log); + } + + /** + * @brief Updates every auction fee except the takeover coordinator-specific splits. + * @note Only the configured management wallet can call this procedure. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(SetAuctionFeesByManagement) + { + output.errorCode = EAuctionError::InvalidInput; + + // Management can update operational fees, but takeover-specific fee parameters stay unchanged. + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.get().management) + { + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFeesByManagement, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + if (!isValidAuctionFeeConfiguration(input.privateAuctionFee, input.publicAuctionCreationFee, input.auctionCancellationFeeBasisPoints, + input.managementFeeBasisPoints, input.developmentFeeBasisPoints, + state.get().takeoverCoordinatorFeeBasisPoints, state.get().shareholderDividendBasisPoints, + input.shareholderFeeBasisPointsTier1, input.shareholderFeeBasisPointsTier2, + input.shareholderFeeBasisPointsTier3, input.shareholderFeeBasisPointsTier4)) + { + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFeesByManagement, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + state.mut().privateAuctionFee = input.privateAuctionFee; + state.mut().publicAuctionCreationFee = input.publicAuctionCreationFee; + state.mut().auctionCancellationFeeBasisPoints = input.auctionCancellationFeeBasisPoints; + state.mut().managementFeeBasisPoints = input.managementFeeBasisPoints; + state.mut().developmentFeeBasisPoints = input.developmentFeeBasisPoints; + state.mut().shareholderFeeBasisPointsTier1 = input.shareholderFeeBasisPointsTier1; + state.mut().shareholderFeeBasisPointsTier2 = input.shareholderFeeBasisPointsTier2; + state.mut().shareholderFeeBasisPointsTier3 = input.shareholderFeeBasisPointsTier3; + state.mut().shareholderFeeBasisPointsTier4 = input.shareholderFeeBasisPointsTier4; + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFeesByManagement, output.errorCode, 0, 0); + logProcedureResult(locals.log); + } + + /** + * @brief Reassigns the management role to another wallet. + * @note Only the configured takeover coordinator can call this procedure. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(SetManagement) + { + output.errorCode = EAuctionError::InvalidInput; + + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.get().takeoverCoordinator) + { + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetManagement, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + if (isZero(input.management)) + { + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetManagement, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + state.mut().management = input.management; + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetManagement, output.errorCode, 0, 0); + logProcedureResult(locals.log); + } + + /** + * @brief Configures the execution fee reserve guard that triggers an emergency pause on a sudden reserve drop. + * @note Only the configured takeover coordinator or management wallet can call this procedure. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(SetFeeReserveGuardConfig) + { + output.errorCode = EAuctionError::InvalidInput; + // Resetting the baseline forces the guard to start a fresh observation window. + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.get().takeoverCoordinator && qpi.invocator() != state.get().management) + { + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetFeeReserveGuardConfig, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + if (input.dropBasisPoints == 0 || input.dropBasisPoints > NOST_BASIS_POINTS_SCALE || input.windowSeconds == 0) + { + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetFeeReserveGuardConfig, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + state.mut().feeReserveGuardDropBasisPoints = input.dropBasisPoints; + state.mut().feeReserveGuardWindowSeconds = input.windowSeconds; + state.mut().feeReserveBaselineAt.setInvalid(); + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetFeeReserveGuardConfig, output.errorCode, 0, 0); + logProcedureResult(locals.log); + } + + /** + * @brief Manually pauses or resumes every auction interaction, overriding the automatic execution fee reserve guard. + * @note Only the configured takeover coordinator or management wallet can call this procedure. Resuming clears the guard window so a stale + * baseline cannot immediately retrigger the pause. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(SetEmergencyPause) + { + output.errorCode = EAuctionError::InvalidInput; + // Manual pause shares the same state as the automatic reserve guard. + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.get().takeoverCoordinator && qpi.invocator() != state.get().management) + { + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetEmergencyPause, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; } - if (qpi.invocationReward() < NOSTROMO_QX_TOKEN_ISSUANCE_FEE) + if (input.paused) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; + state.mut().isEmergencyPaused = 1; + state.mut().emergencyPausedAt = qpi.now(); } - - if (qpi.invocationReward() > NOSTROMO_QX_TOKEN_ISSUANCE_FEE) + else { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - NOSTROMO_QX_TOKEN_ISSUANCE_FEE); + state.mut().isEmergencyPaused = 0; + state.mut().emergencyPausedAt.setInvalid(); + state.mut().feeReserveBaselineAt.setInvalid(); } - locals.tmpProject = state.get().projects.get(input.indexOfProject); - locals.tmpProject.isCreatedFundarasing = 1; - state.mut().projects.set(input.indexOfProject, locals.tmpProject); - - locals.newFundraising.tokenPrice = input.tokenPrice; - locals.newFundraising.soldAmount = input.soldAmount; - locals.newFundraising.requiredFunds = input.requiredFunds; - locals.newFundraising.raisedFunds = 0; - locals.newFundraising.indexOfProject = input.indexOfProject; - locals.newFundraising.firstPhaseStartDate = locals.firstPhaseStartDate; - locals.newFundraising.firstPhaseEndDate = locals.firstPhaseEndDate; - locals.newFundraising.secondPhaseStartDate = locals.secondPhaseStartDate; - locals.newFundraising.secondPhaseEndDate = locals.secondPhaseEndDate; - locals.newFundraising.thirdPhaseStartDate = locals.thirdPhaseStartDate; - locals.newFundraising.thirdPhaseEndDate = locals.thirdPhaseEndDate; - locals.newFundraising.listingStartDate = locals.listingStartDate; - locals.newFundraising.cliffEndDate = locals.cliffEndDate; - locals.newFundraising.vestingEndDate = locals.vestingEndDate; - locals.newFundraising.threshold = input.threshold; - locals.newFundraising.TGE = input.TGE; - locals.newFundraising.stepOfVesting = input.stepOfVesting; - - state.mut().fundaraisings.set(state.get().numberOfFundraising, locals.newFundraising); - state.mut().numberOfFundraising++; + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetEmergencyPause, output.errorCode, 0, 0); + logProcedureResult(locals.log); } - struct investInProject_locals - { - QX::IssueAsset_input input; - QX::IssueAsset_output output; - QX::TransferShareManagementRights_input TransferShareManagementRightsInput; - QX::TransferShareManagementRights_output TransferShareManagementRightsOutput; - investInfo tmpInvestData; - fundaraisingInfo tmpFundraising; - uint64 maxCap, minCap, maxInvestmentPerUser, userInvestedAmount; - uint32 curDate, elementIndex, i, numberOfInvestedProjects; - uint8 tierLevel; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(investInProject) + /** + * @brief Returns the stored state of one auction. + * @note The response contains a serializable auction view; access-control containers are returned as fixed arrays with counts. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionByIndex) { - if (input.indexOfFundraising >= state.get().numberOfFundraising || qpi.invocationReward() == 0) + output.found = 0; + locals.findAuctionInput.auctionIndex = input.auctionIndex; + CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); + if (!locals.findAuctionOutput.found) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; + return; } + locals.auction = locals.findAuctionOutput.auction; - locals.maxCap = state.get().fundaraisings.get(input.indexOfFundraising).requiredFunds + div(state.get().fundaraisings.get(input.indexOfFundraising).requiredFunds * state.get().fundaraisings.get(input.indexOfFundraising).threshold, 100ULL); - locals.minCap = state.get().fundaraisings.get(input.indexOfFundraising).requiredFunds - div(state.get().fundaraisings.get(input.indexOfFundraising).requiredFunds * state.get().fundaraisings.get(input.indexOfFundraising).threshold, 100ULL); - if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects) && locals.numberOfInvestedProjects >= NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST) + output.found = 1; + output.auction.core = locals.auction.core; + + // Hash containers are flattened into arrays because they are not part of the public ABI surface. + output.auction.requiredAccessAssetCount = 0; + for (locals.requiredAccessAssetSetIndex = locals.auction.requiredAccessAssets.nextElementIndex(NULL_INDEX); + locals.requiredAccessAssetSetIndex != NULL_INDEX; + locals.requiredAccessAssetSetIndex = locals.auction.requiredAccessAssets.nextElementIndex(locals.requiredAccessAssetSetIndex)) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; + locals.requiredAccessAsset.asset = locals.auction.requiredAccessAssets.key(locals.requiredAccessAssetSetIndex); + locals.requiredAccessAsset.quantity = locals.auction.requiredAccessAssets.value(locals.requiredAccessAssetSetIndex); + output.auction.requiredAccessAssets.set(output.auction.requiredAccessAssetCount, locals.requiredAccessAsset); + output.auction.requiredAccessAssetCount = sadd(output.auction.requiredAccessAssetCount, 1ULL); } - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); - - locals.tmpFundraising = state.get().fundaraisings.get(input.indexOfFundraising); - - if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).firstPhaseStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).firstPhaseEndDate) + output.auction.allowedBidderWalletCount = 0; + for (locals.allowedBidderWalletSetIndex = locals.auction.allowedBidderWallets.nextElementIndex(NULL_INDEX); + locals.allowedBidderWalletSetIndex != NULL_INDEX; + locals.allowedBidderWalletSetIndex = locals.auction.allowedBidderWallets.nextElementIndex(locals.allowedBidderWalletSetIndex)) { - if (state.get().users.contains(qpi.invocator()) == 0) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - - state.get().users.get(qpi.invocator(), locals.tierLevel); - switch (locals.tierLevel) - { - case 1: - locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT, state.get().totalPoolWeight); - break; - case 2: - locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT, state.get().totalPoolWeight); - break; - case 3: - locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_DOG_POOL_WEIGHT, state.get().totalPoolWeight); - break; - case 4: - locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT, state.get().totalPoolWeight); - break; - case 5: - locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_WARRIOR_POOL_WEIGHT, state.get().totalPoolWeight); - break; - default: - break; - } - - state.get().investors.get(qpi.invocator(), state.mut().tmpInvestedList); - state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects); - - for (locals.i = 0; locals.i < locals.numberOfInvestedProjects; locals.i++) - { - if (state.get().tmpInvestedList.get(locals.i).indexOfFundraising == input.indexOfFundraising) - { - locals.userInvestedAmount = state.get().tmpInvestedList.get(locals.i).investedAmount; - break; - } - } - - locals.tmpInvestData.indexOfFundraising = input.indexOfFundraising; - - if (locals.i < locals.numberOfInvestedProjects) - { - if (locals.tmpFundraising.raisedFunds + locals.maxInvestmentPerUser - locals.userInvestedAmount > locals.maxCap) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - if (qpi.invocationReward() + locals.userInvestedAmount > locals.maxInvestmentPerUser) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() + locals.userInvestedAmount - locals.maxInvestmentPerUser); - - locals.tmpInvestData.investedAmount = locals.maxInvestmentPerUser; - locals.tmpFundraising.raisedFunds += locals.maxInvestmentPerUser - locals.userInvestedAmount; - } - else - { - locals.tmpInvestData.investedAmount = qpi.invocationReward() + locals.userInvestedAmount; - locals.tmpFundraising.raisedFunds += qpi.invocationReward(); - } - state.mut().tmpInvestedList.set(locals.i, locals.tmpInvestData); - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - } - else - { - if (locals.tmpFundraising.raisedFunds + locals.maxInvestmentPerUser > locals.maxCap) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - if (qpi.invocationReward() > (sint64)locals.maxInvestmentPerUser) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.maxInvestmentPerUser); - locals.tmpInvestData.investedAmount = locals.maxInvestmentPerUser; - locals.tmpFundraising.raisedFunds += locals.maxInvestmentPerUser; - } - else - { - locals.tmpInvestData.investedAmount = qpi.invocationReward(); - locals.tmpFundraising.raisedFunds += qpi.invocationReward(); - } - - state.mut().tmpInvestedList.set(locals.numberOfInvestedProjects, locals.tmpInvestData); - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects)) - { - state.mut().numberOfInvestedProjects.set(qpi.invocator(), locals.numberOfInvestedProjects + 1); - } - else - { - state.mut().numberOfInvestedProjects.set(qpi.invocator(), 1); - } - } + locals.allowedBidderWallet = locals.auction.allowedBidderWallets.key(locals.allowedBidderWalletSetIndex); + output.auction.allowedBidderWallets.set(output.auction.allowedBidderWalletCount, locals.allowedBidderWallet); + output.auction.allowedBidderWalletCount = sadd(output.auction.allowedBidderWalletCount, 1ULL); } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).secondPhaseStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).secondPhaseEndDate) - { - if (state.get().users.contains(qpi.invocator()) == 0) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - - state.get().users.get(qpi.invocator(), locals.tierLevel); - if (locals.tierLevel < 4) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - switch (locals.tierLevel) - { - case 4: - locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT, state.get().totalPoolWeight); - break; - case 5: - locals.maxInvestmentPerUser = div(locals.maxCap * NOSTROMO_TIER_WARRIOR_POOL_WEIGHT, state.get().totalPoolWeight); - break; - default: - break; - } - - state.get().investors.get(qpi.invocator(), state.mut().tmpInvestedList); - state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects); + } - for (locals.i = 0; locals.i < locals.numberOfInvestedProjects; locals.i++) + /** + * @brief Returns the stored bid state of one wallet in one auction. + * @note The response indicates whether a participant record exists for the requested auction and wallet. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionParticipant) + { + output.found = 0; + locals.bestParticipantFound = 0; + // A wallet can have multiple historical batch bid slots; return the newest matching record. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) + { + locals.participantData = state.get().participants.get(locals.participantSlotIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex || + locals.participantData.participant != input.participant) { - if (state.get().tmpInvestedList.get(locals.i).indexOfFundraising == input.indexOfFundraising) - { - locals.userInvestedAmount = state.get().tmpInvestedList.get(locals.i).investedAmount; - break; - } + continue; } - locals.tmpInvestData.indexOfFundraising = input.indexOfFundraising; - - if (locals.i < locals.numberOfInvestedProjects) - { - if (locals.tmpFundraising.raisedFunds + locals.maxInvestmentPerUser - locals.userInvestedAmount > locals.maxCap) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - if (qpi.invocationReward() + locals.userInvestedAmount > locals.maxInvestmentPerUser) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() + locals.userInvestedAmount - locals.maxInvestmentPerUser); - - locals.tmpInvestData.investedAmount = locals.maxInvestmentPerUser; - locals.tmpFundraising.raisedFunds += locals.maxInvestmentPerUser - locals.userInvestedAmount; - } - else - { - locals.tmpInvestData.investedAmount = qpi.invocationReward() + locals.userInvestedAmount; - locals.tmpFundraising.raisedFunds += qpi.invocationReward(); - } - state.mut().tmpInvestedList.set(locals.i, locals.tmpInvestData); - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - } - else + if (!locals.bestParticipantFound || locals.participantData.bidIndex > output.participantData.bidIndex) { - if (locals.tmpFundraising.raisedFunds + locals.maxInvestmentPerUser > locals.maxCap) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - if (qpi.invocationReward() > (sint64)locals.maxInvestmentPerUser) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.maxInvestmentPerUser); - locals.tmpInvestData.investedAmount = locals.maxInvestmentPerUser; - locals.tmpFundraising.raisedFunds += locals.maxInvestmentPerUser; - } - else - { - locals.tmpInvestData.investedAmount = qpi.invocationReward(); - locals.tmpFundraising.raisedFunds += qpi.invocationReward(); - } - - state.mut().tmpInvestedList.set(locals.numberOfInvestedProjects, locals.tmpInvestData); - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects)) - { - state.mut().numberOfInvestedProjects.set(qpi.invocator(), locals.numberOfInvestedProjects + 1); - } - else - { - state.mut().numberOfInvestedProjects.set(qpi.invocator(), 1); - } + locals.bestParticipantFound = 1; + locals.bestParticipantSlotIndex = locals.participantSlotIndex; + output.participantData = locals.participantData; + output.found = 1; } } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).thirdPhaseStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).thirdPhaseEndDate) + // Search archived slots as well because displaced and settled bids are removed from the live array. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participantHistory.capacity(); ++locals.participantSlotIndex) { - if (locals.tmpFundraising.raisedFunds + qpi.invocationReward() > locals.maxCap) + locals.participantData = state.get().participantHistory.get(locals.participantSlotIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex || + locals.participantData.participant != input.participant) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; + continue; } - state.get().investors.get(qpi.invocator(), state.mut().tmpInvestedList); - state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects); - - for (locals.i = 0; locals.i < locals.numberOfInvestedProjects; locals.i++) + if (!locals.bestParticipantFound || locals.participantData.bidIndex > output.participantData.bidIndex) { - if (state.get().tmpInvestedList.get(locals.i).indexOfFundraising == input.indexOfFundraising) - { - locals.userInvestedAmount = state.get().tmpInvestedList.get(locals.i).investedAmount; - break; - } + locals.bestParticipantFound = 1; + output.participantData = locals.participantData; + output.found = 1; } + } + } - locals.tmpInvestData.indexOfFundraising = input.indexOfFundraising; - - if (locals.i < locals.numberOfInvestedProjects) - { - locals.tmpInvestData.investedAmount = qpi.invocationReward() + locals.userInvestedAmount; - state.mut().tmpInvestedList.set(locals.i, locals.tmpInvestData); - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - } - else - { - locals.tmpInvestData.investedAmount = qpi.invocationReward(); + /** + * @brief Returns the remaining post-BEGIN_EPOCH pause before auction interactions resume. + * @note This getter exposes the 500-tick launch pause referenced by the auction timing rules. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetTicksBeforeAuctionLaunch) + { + output.ticks = 0; - state.mut().tmpInvestedList.set(locals.numberOfInvestedProjects, locals.tmpInvestData); - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects)) - { - state.mut().numberOfInvestedProjects.set(qpi.invocator(), locals.numberOfInvestedProjects + 1); - } - else - { - state.mut().numberOfInvestedProjects.set(qpi.invocator(), 1); - } - } - locals.tmpFundraising.raisedFunds += qpi.invocationReward(); - } - else + if (!state.get().isPostBeginEpochPauseArmed) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; + return; } - if (locals.minCap <= locals.tmpFundraising.raisedFunds && locals.tmpFundraising.isCreatedToken == 0) - { - locals.input.assetName = state.get().projects.get(locals.tmpFundraising.indexOfProject).tokenName; - locals.input.numberOfDecimalPlaces = 0; - locals.input.numberOfShares = state.get().projects.get(locals.tmpFundraising.indexOfProject).supplyOfToken; - locals.input.unitOfMeasurement = 0; - INVOKE_OTHER_CONTRACT_PROCEDURE(QX, IssueAsset, locals.input, locals.output, NOSTROMO_QX_TOKEN_ISSUANCE_FEE); + output.ticks = static_cast(max(static_cast(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) - + (static_cast(qpi.tick()) - static_cast(qpi.initialTick())), + 0)); + } - if (locals.output.issuedNumberOfShares == state.get().projects.get(locals.tmpFundraising.indexOfProject).supplyOfToken) - { - locals.tmpFundraising.isCreatedToken = 1; + /** + * @brief Returns the current auction fee configuration stored in contract state. + * @note The response includes creation, cancellation, revenue split, and tier-based shareholder fee parameters. + */ + PUBLIC_FUNCTION(GetAuctionFees) + { + output.privateAuctionFee = state.get().privateAuctionFee; + output.auctionCancellationFeeBasisPoints = state.get().auctionCancellationFeeBasisPoints; + output.managementFeeBasisPoints = state.get().managementFeeBasisPoints; + output.developmentFeeBasisPoints = state.get().developmentFeeBasisPoints; + output.takeoverCoordinatorFeeBasisPoints = state.get().takeoverCoordinatorFeeBasisPoints; + output.shareholderDividendBasisPoints = state.get().shareholderDividendBasisPoints; + output.shareholderFeeBasisPointsTier1 = state.get().shareholderFeeBasisPointsTier1; + output.shareholderFeeBasisPointsTier2 = state.get().shareholderFeeBasisPointsTier2; + output.shareholderFeeBasisPointsTier3 = state.get().shareholderFeeBasisPointsTier3; + output.shareholderFeeBasisPointsTier4 = state.get().shareholderFeeBasisPointsTier4; + output.publicAuctionCreationFee = state.get().publicAuctionCreationFee; + } - locals.TransferShareManagementRightsInput.asset.assetName = state.get().projects.get(locals.tmpFundraising.indexOfProject).tokenName; - locals.TransferShareManagementRightsInput.asset.issuer = SELF; - locals.TransferShareManagementRightsInput.newManagingContractIndex = SELF_INDEX; - locals.TransferShareManagementRightsInput.numberOfShares = state.get().projects.get(locals.tmpFundraising.indexOfProject).supplyOfToken; + /** + * @brief Calculates the escrow, accumulated fee, and reward required by Batch Auction bid arithmetic. + * @param input Prospective bid quantity and price per asset; zero values are accepted for arithmetic inspection. + * @param output Saturating escrow product, small-bid fee, and saturating total reward. + * @note Non-zero escrow pays enough fee to reach `NOST_BATCH_BID_FEE_CUTOFF`; escrow at or above the cutoff pays no bid fee. + * @note This function does not validate whether `PlaceBid` would accept the bid or mutate contract state. + */ + PUBLIC_FUNCTION(CalculateBatchAuctionBidFee) { calculateBatchAuctionBidFee(input.bidQuantity, input.bidAmount, output); } - INVOKE_OTHER_CONTRACT_PROCEDURE(QX, TransferShareManagementRights, locals.TransferShareManagementRightsInput, locals.TransferShareManagementRightsOutput, 0); + /** + * @brief Returns the current wallets that receive auction fee transfers. + * @note The response exposes the configured management, development, and takeover coordinator addresses. + */ + PUBLIC_FUNCTION(GetFeeRecipients) + { + output.management = state.get().management; + output.development = state.get().development; + output.takeoverCoordinator = state.get().takeoverCoordinator; + } - qpi.transferShareOwnershipAndPossession(state.get().projects.get(locals.tmpFundraising.indexOfProject).tokenName, SELF, SELF, SELF, state.get().projects.get(locals.tmpFundraising.indexOfProject).supplyOfToken - locals.tmpFundraising.soldAmount, state.get().projects.get(locals.tmpFundraising.indexOfProject).creator); + /** + * @brief Returns the ring buffer with recently closed auctions. + * @note The buffer stores auction identifiers for both finalized and cancelled auctions. + * @note When `totalEntries` exceeds `NOST_AUCTION_HISTORY_NUM`, older entries are overwritten in ring-buffer order. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetClosedAuctionHistory) + { + // Preserve physical ring positions to keep the existing auctionIndices ABI stable for clients. + for (locals.historyIndex = 0; locals.historyIndex < state.get().closedAuctionHistory.capacity(); ++locals.historyIndex) + { + locals.auction = state.get().closedAuctionHistory.get(locals.historyIndex); + if (locals.auction.core.status != EAuctionStatus::None) + { + output.auctionIndices.set(locals.historyIndex, locals.auction.core.auctionIndex); } } + output.totalEntries = state.get().closedAuctionHistoryCounter; + } - state.mut().fundaraisings.set(input.indexOfFundraising, locals.tmpFundraising); + /** + * @brief Returns whether the temporary fee override routes every fee to development. + */ + PUBLIC_FUNCTION(GetRouteAllFeesToDevelopment) { output.enabled = state.get().routeAllFeesToDevelopment; } + + /** + * @brief Returns the aggregate shared fee amount awaiting `END_EPOCH` settlement. + * @note The legacy field name is retained for ABI compatibility. + */ + PUBLIC_FUNCTION(GetPendingServiceFeePool) { output.pendingServiceFeePool = getNostromoFeePoolTotal(state.get().feePool); } + /** @brief Returns every accumulator in the shared Nostromo fee pool. */ + PUBLIC_FUNCTION(GetNostromoFeePool) + { + output.feePool = state.get().feePool; + output.totalAmount = getNostromoFeePoolTotal(state.get().feePool); } - struct claimToken_locals + /** @brief Returns the QU obligation currently registered for one wallet. */ + PUBLIC_FUNCTION(GetPendingPayout) { - investInfo tmpInvestData; - uint64 maxClaimAmount, investedAmount, dayA, dayB, start_cur_diffSecond, cur_end_diffSecond, claimedAmount; - uint32 curDate, tmpDate, numberOfInvestedProjects; - sint32 i, j; - uint8 curVestingStep, vestingPercent; - }; + output.amount = 0; + state.get().pendingQuPayouts.get(input.account, output.amount); + } - PUBLIC_PROCEDURE_WITH_LOCALS(claimToken) + /** + * @brief Returns the current state of the execution fee reserve guard, including a live reserve reading. + */ + PUBLIC_FUNCTION(GetFeeReserveGuardState) { - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); + output.currentFeeReserve = qpi.queryFeeReserve(SELF_INDEX); + output.feeReserveBaseline = state.get().feeReserveBaseline; + output.feeReserveBaselineAt = state.get().feeReserveBaselineAt; + output.emergencyPausedAt = state.get().emergencyPausedAt; + output.dropBasisPoints = state.get().feeReserveGuardDropBasisPoints; + output.windowSeconds = state.get().feeReserveGuardWindowSeconds; + output.isEmergencyPaused = state.get().isEmergencyPaused; + } - if (input.indexOfFundraising >= state.get().numberOfFundraising) + /** + * @brief Returns aggregate auction, participant, fee, and pause counters. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetContractStats) + { + output.stats.totalAuctionsCreated = state.get().totalAuctionsCreated; + output.stats.closedAuctionHistoryCounter = state.get().closedAuctionHistoryCounter; + output.stats.auctionShareholderDividendPool = state.get().auctionShareholderDividendPool; + output.stats.pendingServiceFeePool = getNostromoFeePoolTotal(state.get().feePool); + output.stats.totalPendingQuPayouts = state.get().totalPendingQuPayouts; + output.stats.retainedClosedAuctionCount = min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()); + output.stats.retainedParticipantHistoryCount = min(state.get().participantHistoryCounter, state.get().participantHistory.capacity()); + output.stats.finalizedAuctionCount = state.get().totalFinalizedAuctions; + output.stats.cancelledAuctionCount = state.get().totalCancelledAuctions; + output.stats.qxTransferFee = state.get().qxTransferFee; + output.stats.routeAllFeesToDevelopment = state.get().routeAllFeesToDevelopment; + output.stats.isAuctionTimerPaused = state.get().isAuctionTimerPaused; + output.stats.isPostBeginEpochPauseArmed = state.get().isPostBeginEpochPauseArmed; + output.stats.isEmergencyPaused = state.get().isEmergencyPaused; + + // Stats scan fixed storage because participant slots and auction records are not separately indexed by status. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) { - return ; + locals.participantData = state.get().participants.get(locals.participantSlotIndex); + if (locals.participantData.isUsed) + { + output.stats.participantCount = sadd(output.stats.participantCount, 1ULL); + } } - state.get().investors.get(qpi.invocator(), state.mut().tmpInvestedList); - if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects) == 0) + for (locals.auctionElementIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); locals.auctionElementIndex != NULL_INDEX; + locals.auctionElementIndex = state.get().auctionList.nextElementIndex(locals.auctionElementIndex)) { - return ; + locals.auction = state.get().auctionList.value(locals.auctionElementIndex); + switch (locals.auction.core.status) + { + case EAuctionStatus::Active: output.stats.activeAuctionCount = sadd(output.stats.activeAuctionCount, 1ULL); break; + case EAuctionStatus::PendingSellerDecision: + output.stats.pendingSellerDecisionAuctionCount = sadd(output.stats.pendingSellerDecisionAuctionCount, 1ULL); + break; + default: break; + } } + } - for (locals.i = 0; locals.i < (sint32)locals.numberOfInvestedProjects; locals.i++) + /** + * @brief Returns a page of auction summaries ordered by creation index. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionSummaries) + { + // Live and archived records are disjoint, so the retained total does not require an ordered scan. + output.totalCount = + sadd(state.get().auctionList.population(), min(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity())); + output.returnedCount = 0; + locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + if (locals.boundedLimit == 0 || input.offset >= output.totalCount) + { + return; + } + locals.scannedAuctionCount = 0; + locals.selectNextAuctionInput.hasAfterAuctionIndex = 0; + locals.selectNextAuctionInput.includeClosedAuctions = 1; + locals.selectNextAuctionInput.filterBySeller = 0; + // Cursor selection reconstructs creation order across unordered live storage and the closed-history ring. + while (output.returnedCount < locals.boundedLimit && locals.scannedAuctionCount < output.totalCount) { - if (state.get().tmpInvestedList.get(locals.i).indexOfFundraising == input.indexOfFundraising) + CALL(SelectNextRetainedAuction, locals.selectNextAuctionInput, locals.selectNextAuctionOutput); + if (!locals.selectNextAuctionOutput.found) { - locals.investedAmount = state.get().tmpInvestedList.get(locals.i).investedAmount; - locals.claimedAmount = state.get().tmpInvestedList.get(locals.i).claimedAmount; - locals.tmpInvestData = state.get().tmpInvestedList.get(locals.i); break; } + locals.auction = locals.selectNextAuctionOutput.auction; + // Skip only the requested prefix; the exact total is already available without scanning the remainder. + if (locals.scannedAuctionCount >= input.offset) + { + fillAuctionSummary(locals.auction, locals.auctionSummary); + output.auctions.set(output.returnedCount, locals.auctionSummary); + output.returnedCount = sadd(output.returnedCount, 1ULL); + } + locals.scannedAuctionCount = sadd(locals.scannedAuctionCount, 1ULL); + locals.selectNextAuctionInput.afterAuctionIndex = locals.auction.core.auctionIndex; + locals.selectNextAuctionInput.hasAfterAuctionIndex = 1; } + } - if (locals.i == locals.numberOfInvestedProjects) + /** + * @brief Returns a page of active or pending-seller-decision auction indices. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetActiveAuctionIndices) + { + // Invariant: terminal auctions are archived and removed, so every live-map entry is active or awaiting a seller decision. + output.totalCount = state.get().auctionList.population(); + output.returnedCount = 0; + locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + if (locals.boundedLimit == 0 || input.offset >= output.totalCount) { - return ; + return; } - if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).listingStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate) + locals.scannedAuctionCount = 0; + locals.selectNextAuctionInput.hasAfterAuctionIndex = 0; + locals.selectNextAuctionInput.includeClosedAuctions = 0; + locals.selectNextAuctionInput.filterBySeller = 0; + // Select only the requested live-map prefix and page; closed history cannot contain active auctions. + while (output.returnedCount < locals.boundedLimit && locals.scannedAuctionCount < output.totalCount) { - locals.maxClaimAmount = div(div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice) * state.get().fundaraisings.get(input.indexOfFundraising).TGE, 100ULL); + CALL(SelectNextRetainedAuction, locals.selectNextAuctionInput, locals.selectNextAuctionOutput); + if (!locals.selectNextAuctionOutput.found) + { + break; + } + locals.selectNextAuctionInput.afterAuctionIndex = locals.selectNextAuctionOutput.auction.core.auctionIndex; + locals.selectNextAuctionInput.hasAfterAuctionIndex = 1; + if (locals.scannedAuctionCount >= input.offset) + { + output.auctionIndices.set(output.returnedCount, locals.selectNextAuctionOutput.auction.core.auctionIndex); + output.returnedCount = sadd(output.returnedCount, 1ULL); + } + locals.scannedAuctionCount = sadd(locals.scannedAuctionCount, 1ULL); } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate) - { - locals.tmpDate = state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate; - diffDateInSecond(locals.tmpDate, locals.curDate, locals.j, locals.dayA, locals.dayB, locals.start_cur_diffSecond); - locals.tmpDate = state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate; - diffDateInSecond(locals.curDate, locals.tmpDate, locals.j, locals.dayA, locals.dayB, locals.cur_end_diffSecond); + } - locals.curVestingStep = (uint8)div(locals.start_cur_diffSecond, div(locals.start_cur_diffSecond + locals.cur_end_diffSecond, state.get().fundaraisings.get(input.indexOfFundraising).stepOfVesting * 1ULL)) + 1; - locals.vestingPercent = (uint8)div(100ULL - state.get().fundaraisings.get(input.indexOfFundraising).TGE, state.get().fundaraisings.get(input.indexOfFundraising).stepOfVesting * 1ULL) * locals.curVestingStep; - locals.maxClaimAmount = div(div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice) * (state.get().fundaraisings.get(input.indexOfFundraising).TGE + locals.vestingPercent), 100ULL); - } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate) + /** + * @brief Returns a page of auction summaries created by a seller. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionsBySeller) + { + output.returnedCount = 0; + locals.countAuctionsInput.seller = input.seller; + CALL(CountRetainedAuctionsBySeller, locals.countAuctionsInput, locals.countAuctionsOutput); + output.totalCount = locals.countAuctionsOutput.count; + locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + if (locals.boundedLimit == 0 || input.offset >= output.totalCount) { - locals.maxClaimAmount = div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice); + return; } - if (input.amount + locals.claimedAmount > locals.maxClaimAmount) - { - return ; - } - else + locals.scannedAuctionCount = 0; + locals.selectNextAuctionInput.seller = input.seller; + locals.selectNextAuctionInput.hasAfterAuctionIndex = 0; + locals.selectNextAuctionInput.includeClosedAuctions = 1; + locals.selectNextAuctionInput.filterBySeller = 1; + // The selector skips other sellers, so only the requested seller's prefix and page are ordered. + while (output.returnedCount < locals.boundedLimit && locals.scannedAuctionCount < output.totalCount) { - qpi.transferShareOwnershipAndPossession(state.get().projects.get(state.get().fundaraisings.get(input.indexOfFundraising).indexOfProject).tokenName, SELF, SELF, SELF, input.amount, qpi.invocator()); - if (input.amount + locals.claimedAmount == locals.maxClaimAmount && state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate <= locals.curDate) + CALL(SelectNextRetainedAuction, locals.selectNextAuctionInput, locals.selectNextAuctionOutput); + if (!locals.selectNextAuctionOutput.found) { - state.mut().tmpInvestedList.set(locals.i, state.get().tmpInvestedList.get(locals.numberOfInvestedProjects - 1)); - state.mut().numberOfInvestedProjects.set(qpi.invocator(), locals.numberOfInvestedProjects - 1); - } - else - { - locals.tmpInvestData.claimedAmount = input.amount + locals.claimedAmount; - state.mut().tmpInvestedList.set(locals.i, locals.tmpInvestData); + break; } - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects); - if (locals.numberOfInvestedProjects == 0) + locals.auction = locals.selectNextAuctionOutput.auction; + locals.selectNextAuctionInput.afterAuctionIndex = locals.auction.core.auctionIndex; + locals.selectNextAuctionInput.hasAfterAuctionIndex = 1; + if (locals.scannedAuctionCount >= input.offset) { - state.mut().investors.removeByKey(qpi.invocator()); - state.mut().numberOfInvestedProjects.removeByKey(qpi.invocator()); + fillAuctionSummary(locals.auction, locals.auctionSummary); + output.auctions.set(output.returnedCount, locals.auctionSummary); + output.returnedCount = sadd(output.returnedCount, 1ULL); } - output.claimedAmount = input.amount; + locals.scannedAuctionCount = sadd(locals.scannedAuctionCount, 1ULL); } } - struct upgradeTier_locals + /** + * @brief Looks up the first auction matching a metadata CID. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionByMetadataCid) { - uint64 deltaAmount; - uint32 i, deltaPoolWeight; - uint8 currentTierLevel; - }; + output.found = 0; + output.auctionIndex = 0; + locals.findAuctionInput.metadataIpfsCid = input.metadataIpfsCid; + CALL(FindFirstRetainedAuctionByMetadataCid, locals.findAuctionInput, locals.findAuctionOutput); + // The helper compares all retained candidates and returns the smallest matching creation index. + if (!locals.findAuctionOutput.found) + { + return; + } + output.found = 1; + output.auctionIndex = locals.findAuctionOutput.auction.core.auctionIndex; + fillAuctionSummary(locals.findAuctionOutput.auction, output.auction); + } - PUBLIC_PROCEDURE_WITH_LOCALS(upgradeTier) + /** + * @brief Returns auction summaries for a batch of requested indices. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionSummariesByIndexBatch) { - if (state.get().users.contains(qpi.invocator()) == 0) + output.returnedCount = 0; + locals.boundedLimit = min(input.count, NOST_AUCTION_GETTER_PAGE_SIZE); + // Preserve input positions so callers can correlate each requested index with its found flag. + for (locals.requestedIndex = 0; locals.requestedIndex < locals.boundedLimit; ++locals.requestedIndex) { - if (qpi.invocationReward() > 0) + locals.auctionIndex = input.auctionIndices.get(locals.requestedIndex); + locals.findAuctionInput.auctionIndex = locals.auctionIndex; + CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); + if (locals.findAuctionOutput.found) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + locals.auction = locals.findAuctionOutput.auction; + fillAuctionSummary(locals.auction, locals.auctionSummary); + output.auctions.set(locals.requestedIndex, locals.auctionSummary); + output.found.set(locals.requestedIndex, 1); + output.returnedCount = sadd(output.returnedCount, 1ULL); } - return ; } + } - state.get().users.get(qpi.invocator(), locals.currentTierLevel); - - switch (locals.currentTierLevel) - { - case 1: - locals.deltaAmount = NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT - NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT; - locals.deltaPoolWeight = NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT - NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT; - break; - case 2: - locals.deltaAmount = NOSTROMO_TIER_DOG_STAKE_AMOUNT - NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT; - locals.deltaPoolWeight = NOSTROMO_TIER_DOG_POOL_WEIGHT - NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; - break; - case 3: - locals.deltaAmount = NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT - NOSTROMO_TIER_DOG_STAKE_AMOUNT; - locals.deltaPoolWeight = NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT - NOSTROMO_TIER_DOG_POOL_WEIGHT; - break; - case 4: - locals.deltaAmount = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT - NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT; - locals.deltaPoolWeight = NOSTROMO_TIER_WARRIOR_POOL_WEIGHT - NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; - break; - default: - break; - } - if (input.newTierLevel != locals.currentTierLevel + 1 || qpi.invocationReward() < (sint64)locals.deltaAmount) + /** + * @brief Returns a page of participants for one auction. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionParticipants) + { + output.totalCount = 0; + output.returnedCount = 0; + locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + // Participant storage is global, so auction participant pages are built by scanning all slots. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) { - if (qpi.invocationReward() > 0) + locals.participantData = state.get().participants.get(locals.participantSlotIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + continue; + } + if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) + { + fillParticipantSummary(locals.participantData, locals.participantSummary); + output.participants.set(output.returnedCount, locals.participantSummary); + output.returnedCount = sadd(output.returnedCount, 1ULL); } - return ; + output.totalCount = sadd(output.totalCount, 1ULL); } - else + // Append archived records after live records so an offset spans both storage tiers deterministically. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participantHistory.capacity(); ++locals.participantSlotIndex) { - state.mut().users.set(qpi.invocator(), input.newTierLevel); - if (qpi.invocationReward() > (sint64)locals.deltaAmount) + locals.participantData = state.get().participantHistory.get(locals.participantSlotIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) + { + continue; + } + if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.deltaAmount); + fillParticipantSummary(locals.participantData, locals.participantSummary); + output.participants.set(output.returnedCount, locals.participantSummary); + output.returnedCount = sadd(output.returnedCount, 1ULL); } - state.mut().totalPoolWeight += locals.deltaPoolWeight; + output.totalCount = sadd(output.totalCount, 1ULL); } } - PUBLIC_PROCEDURE(TransferShareManagementRights) + /** + * @brief Returns a page of historical auction participations for one wallet. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetUserParticipations) { - if (qpi.invocationReward() < state.get().transferRightsFee) - { - return ; - } - - if (qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer,qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.numberOfShares) + output.totalCount = 0; + output.returnedCount = 0; + locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + // User participation history includes inactive records so settled and displaced bids remain visible. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) { - // not enough shares available - output.transferredNumberOfShares = 0; - if (qpi.invocationReward() > 0) + locals.participantData = state.get().participants.get(locals.participantSlotIndex); + if (!locals.participantData.isUsed || locals.participantData.participant != input.participant) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + continue; + } + if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) + { + fillUserParticipationSummary(locals.participantData.auctionIndex, locals.participantData, locals.userParticipationSummary); + output.participations.set(output.returnedCount, locals.userParticipationSummary); + output.returnedCount = sadd(output.returnedCount, 1ULL); } + output.totalCount = sadd(output.totalCount, 1ULL); } - else + // Continue the same page over archived bids after accounting for matching live entries. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participantHistory.capacity(); ++locals.participantSlotIndex) { - if (qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, - input.newManagingContractIndex, input.newManagingContractIndex, state.get().transferRightsFee) < 0) + locals.participantData = state.get().participantHistory.get(locals.participantSlotIndex); + if (!locals.participantData.isUsed || locals.participantData.participant != input.participant) { - // error - output.transferredNumberOfShares = 0; - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } + continue; } - else + if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) { - // success - output.transferredNumberOfShares = input.numberOfShares; - if (qpi.invocationReward() > state.get().transferRightsFee) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.get().transferRightsFee); - } + fillUserParticipationSummary(locals.participantData.auctionIndex, locals.participantData, locals.userParticipationSummary); + output.participations.set(output.returnedCount, locals.userParticipationSummary); + output.returnedCount = sadd(output.returnedCount, 1ULL); } + output.totalCount = sadd(output.totalCount, 1ULL); } } - PUBLIC_FUNCTION(getStats) + /** + * @brief Returns the most recently created auction index when one exists. + */ + PUBLIC_FUNCTION(GetLatestAuctionIndex) { - output.epochRevenue = state.get().epochRevenue; - output.numberOfCreatedProject = state.get().numberOfCreatedProject; - output.numberOfFundraising = state.get().numberOfFundraising; - output.numberOfRegister = state.get().numberOfRegister; - output.totalPoolWeight = state.get().totalPoolWeight; + output.found = state.get().totalAuctionsCreated > 0; + output.auctionIndex = output.found ? state.get().totalAuctionsCreated - 1 : 0; } - PUBLIC_FUNCTION(getTierLevelByUser) + /** + * @brief Counts auctions created by a seller. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionCountBySeller) { - state.get().users.get(input.userId, output.tierLevel); + locals.countAuctionsInput.seller = input.seller; + CALL(CountRetainedAuctionsBySeller, locals.countAuctionsInput, locals.countAuctionsOutput); + output.count = locals.countAuctionsOutput.count; } - PUBLIC_FUNCTION(getUserVoteStatus) + /** + * @brief Returns immutable creation-time fields for an auction. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionAtCreationSnapshot) { - state.get().numberOfVotedProject.get(input.userId, output.numberOfVotedProjects); - state.get().voteStatus.get(input.userId, output.projectIndexList); + output.found = 0; + locals.findAuctionInput.auctionIndex = input.auctionIndex; + CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); + if (!locals.findAuctionOutput.found) + { + return; + } + locals.auction = locals.findAuctionOutput.auction; + output.found = 1; + output.seller = locals.auction.core.seller; + output.createdAt = locals.auction.core.createdAt; + output.auctionIndex = locals.auction.core.auctionIndex; + output.quantityForSale = locals.auction.core.quantityForSale; + output.initialPrice = locals.auction.core.initialPrice; + output.salePrice = locals.auction.core.salePrice; + output.minimumBidIncrement = locals.auction.core.minimumBidIncrement; + output.buyNowPrice = locals.auction.core.buyNowPrice; + output.auctionDurationSeconds = locals.auction.core.auctionDurationSeconds; + output.type = static_cast(locals.auction.core.type); + output.visibility = static_cast(locals.auction.core.visibility); } - PUBLIC_FUNCTION(checkTokenCreatability) + /** + * @brief Returns current read-only guidance for the next valid Batch Auction bid. + * @note `found` also covers closed auctions while their snapshots remain in retained history. + * @note `PlaceBid` re-runs the same availability validation before accepting a bid. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetBatchAuctionBidAvailability) { - output.result = state.get().tokens.contains(input.tokenName); - } + locals.computeBatchBidAvailabilityInput.auctionIndex = input.auctionIndex; + locals.computeBatchBidAvailabilityInput.bidAmount = 0; + CALL(ComputeBatchBidAvailability, locals.computeBatchBidAvailabilityInput, output); + // Live auctions are fully classified by the availability helper, including non-Batch auctions. + if (output.found) + { + return; + } - PUBLIC_FUNCTION(getNumberOfInvestedProjects) - { - state.get().numberOfInvestedProjects.get(input.userId, output.numberOfInvestedProjects); + // A retained closed auction still exists for lookup purposes, but can never accept another bid. + locals.isClosedAuctionRetainedInput.auctionIndex = input.auctionIndex; + CALL(IsClosedAuctionRetained, locals.isClosedAuctionRetainedInput, locals.isClosedAuctionRetainedOutput); + output.found = locals.isClosedAuctionRetainedOutput.found; } -public: - struct getProjectByIndex_input + /** + * @brief Transfers share management rights for an asset position to another managing contract. + * @note The caller must currently possess at least the requested number of shares. + * @note The caller must send the destination contract's required transfer fee as invocation reward. This contract cannot query that + * fee before calling `releaseShares`, so callers must resolve it from `newManagingContractIndex`. + */ + PUBLIC_PROCEDURE_WITH_LOCALS(TransferShareManagementRights) { - uint32 indexOfProject; - }; + locals.reward = qpi.invocationReward(); + locals.refundAmount = locals.reward; + locals.success = false; + output.transferredNumberOfShares = 0; + output.errorCode = EAuctionError::InvalidInput; + + // Emergency pause blocks cross-contract share release and returns the caller's fee budget. + if (state.get().isEmergencyPaused) + { + if (locals.refundAmount > 0) + { + qpi.transfer(qpi.invocator(), locals.refundAmount); + } + output.errorCode = EAuctionError::AuctionPaused; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::TransferShareManagementRights, output.errorCode, 0, + output.transferredNumberOfShares); + logProcedureResult(locals.log); + return; + } - struct getProjectByIndex_output - { - projectInfo project; - }; + // `releaseShares` consumes only the destination transfer fee; any unused reward is refunded below. + if (input.numberOfShares > 0 && qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer, qpi.invocator(), qpi.invocator(), + SELF_INDEX, SELF_INDEX) >= input.numberOfShares) + { + locals.result = qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, input.newManagingContractIndex, + input.newManagingContractIndex, locals.reward); + if (locals.result != INVALID_AMOUNT && locals.result >= 0) + { + locals.success = true; + locals.refundAmount = locals.reward - locals.result; + } + } - PUBLIC_FUNCTION(getProjectByIndex) - { - output.project = state.get().projects.get(input.indexOfProject); + if (locals.success) + { + output.transferredNumberOfShares = input.numberOfShares; + output.errorCode = EAuctionError::Success; + } + + if (locals.refundAmount > 0) + { + qpi.transfer(qpi.invocator(), locals.refundAmount); + } + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::TransferShareManagementRights, output.errorCode, 0, + output.transferredNumberOfShares); + + logProcedureResult(locals.log); } - struct getFundarasingByIndex_input +protected: + /** + * @brief Emits a procedure log as success or error based on its error code. + */ + static void logProcedureResult(const NostromoProcedureLog& log) { - uint32 indexOfFundarasing; - }; + if (log.errorCode == static_cast(EAuctionError::Success)) + { + LOG_INFO(log); + } + else + { + LOG_ERROR(log); + } + } - struct getFundarasingByIndex_output + /** + * @brief Fills the common procedure log payload. + */ + static void setProcedureLogInput(NostromoProcedureLog& log, const id& actor, EProcedureId procedure, EAuctionError errorCode, uint64 auctionIndex, + sint64 amount) { - fundaraisingInfo fundarasing; - }; + log.contractIndex = SELF_INDEX; + log.procedure = static_cast(procedure); + log.errorCode = static_cast(errorCode); + log.auctionIndex = auctionIndex; + log.actor = actor; + log.amount = amount; + log._terminator = 0; + } - PUBLIC_FUNCTION(getFundarasingByIndex) + /** + * @brief Copies persisted auction data into a compact summary. + */ + static void fillAuctionSummary(const AuctionData& auction, AuctionSummary& summary) { - output.fundarasing = state.get().fundaraisings.get(input.indexOfFundarasing); + summary.metadataIpfsCid = auction.core.metadataIpfsCid; + summary.seller = auction.core.seller; + summary.highestBidder = auction.core.highestBidder; + summary.createdAt = auction.core.createdAt; + summary.settledAt = auction.core.settledAt; + summary.auctionIndex = auction.core.auctionIndex; + summary.quantityForSale = auction.core.quantityForSale; + summary.allocatedQuantity = auction.core.allocatedQuantity; + summary.initialPrice = auction.core.initialPrice; + summary.salePrice = auction.core.salePrice; + summary.buyNowPrice = auction.core.buyNowPrice; + summary.highestBidPrice = auction.core.highestBidPrice; + summary.highestBidQuantity = auction.core.highestBidQuantity; + summary.highestBidAmount = auction.core.highestBidAmount; + summary.type = static_cast(auction.core.type); + summary.visibility = static_cast(auction.core.visibility); + summary.status = static_cast(auction.core.status); } - struct getProjectIndexListByCreator_input + /** + * @brief Copies participant storage data into an auction participant summary. + */ + static void fillParticipantSummary(const AuctionParticipantData& participantData, ParticipantSummary& summary) { - id creator; - }; + summary.participant = participantData.participant; + summary.lastBidTime = participantData.lastBidTime; + summary.bidAmount = participantData.bidAmount; + summary.escrowedAmount = participantData.escrowedAmount; + summary.requestedQuantity = participantData.requestedQuantity; + summary.allocatedQuantity = participantData.allocatedQuantity; + summary.isWinningBid = participantData.isWinningBid; + } - struct getProjectIndexListByCreator_output + /** + * @brief Copies participant storage data into a user participation summary. + */ + static void fillUserParticipationSummary(uint64 auctionIndex, const AuctionParticipantData& participantData, UserParticipationSummary& summary) { - Array indexListForProjects; - }; + summary.participant = participantData.participant; + summary.lastBidTime = participantData.lastBidTime; + summary.auctionIndex = auctionIndex; + summary.bidAmount = participantData.bidAmount; + summary.escrowedAmount = participantData.escrowedAmount; + summary.requestedQuantity = participantData.requestedQuantity; + summary.allocatedQuantity = participantData.allocatedQuantity; + summary.isWinningBid = participantData.isWinningBid; + } - struct getProjectIndexListByCreator_locals + /** + * @brief Returns the smaller of two values. + */ + template + static constexpr T min(const T& a, const T& b) { - uint32 i, countOfProject; - }; + return (a < b) ? a : b; + } + /** + * @brief Returns the larger of two values. + */ + template + static constexpr T max(const T& a, const T& b) + { + return a > b ? a : b; + } - PUBLIC_FUNCTION_WITH_LOCALS(getProjectIndexListByCreator) + /** + * @brief Resolves Batch Auction quantity invariants from creation input. + */ + static bool resolveBatchAuctionCreateParams(uint64 lotItemCount, uint64 totalEscrowQuantity, uint64 minimumPurchaseQuantity, + uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPrice) { - for (locals.i = 0; locals.i < state.get().numberOfCreatedProject; locals.i++) - { - if (state.get().projects.get(locals.i).creator == input.creator) - { - output.indexListForProjects.set(locals.countOfProject++, locals.i); - } - } - for (locals.i = locals.countOfProject; locals.i < NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST; locals.i++) + quantityForSale = 0; + resolvedMinimumPurchaseQuantity = 0; + if (lotItemCount != NOST_BATCH_AUCTION_LOT_ITEM_NUM || totalEscrowQuantity == 0 || minimumPurchaseQuantity == 0 || + minimumPurchaseQuantity > totalEscrowQuantity || buyNowPrice != 0) { - output.indexListForProjects.set(locals.i, NOSTROMO_MAX_NUMBER_PROJECT); + return false; } + quantityForSale = totalEscrowQuantity; + resolvedMinimumPurchaseQuantity = minimumPurchaseQuantity; + return true; } - struct getInfoUserInvested_input + /** + * @brief Resolves Standard Auction quantity and price invariants from creation input. + */ + static bool resolveStandardAuctionCreateParams(uint64 minimumBidIncrement, uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity, + uint64 buyNowPrice, uint64 initialPrice, uint64 salePrice) { - id investorId; - }; + quantityForSale = 0; + resolvedMinimumPurchaseQuantity = 0; + if (initialPrice < NOST_STANDARD_MIN_PRICE || salePrice < NOST_STANDARD_MIN_PRICE || minimumBidIncrement < NOST_STANDARD_MIN_BID_INCREMENT) + { + return false; + } - struct getInfoUserInvested_output - { - Array listUserInvested; - }; + if (initialPrice > salePrice) + { + return false; + } - struct getInfoUserInvested_locals - { - uint32 i, countOfProject; - }; + if (buyNowPrice > 0 && (buyNowPrice < initialPrice || buyNowPrice < salePrice)) + { - PUBLIC_FUNCTION_WITH_LOCALS(getInfoUserInvested) - { - state.get().investors.get(input.investorId, output.listUserInvested); - } + return false; + } - struct getMaxClaimAmount_input - { - id investorId; - uint32 indexOfFundraising; - }; + quantityForSale = NOST_STANDARD_AUCTION_LOT_COUNT; + resolvedMinimumPurchaseQuantity = 0; + return true; + } - struct getMaxClaimAmount_output + /** + * @brief Validates that private auctions use at least one supported access mode. + */ + constexpr static bool validatePrivateAuctionAccess(EAuctionVisibility visibility, uint64 requiredAccessAssetCount, uint64 allowedWalletCount) { - uint64 amount; - }; + return visibility != EAuctionVisibility::Private || requiredAccessAssetCount > 0 || allowedWalletCount > 0; + } - struct getMaxClaimAmount_locals + /** + * @brief Validates governance fee percentages and fixed service fees. + */ + constexpr static bool isValidAuctionFeeConfiguration(sint64 privateAuctionFee, sint64 publicAuctionCreationFee, + uint64 auctionCancellationFeeBasisPoints, uint64 managementFeeBasisPoints, + uint64 developmentFeeBasisPoints, uint64 takeoverCoordinatorFeeBasisPoints, + uint64 shareholderDividendBasisPoints, uint64 shareholderFeeBasisPointsTier1, + uint64 shareholderFeeBasisPointsTier2, uint64 shareholderFeeBasisPointsTier3, + uint64 shareholderFeeBasisPointsTier4) { - Array tmpInvestedList; - investInfo tmpInvestData; - uint64 maxClaimAmount, investedAmount, dayA, dayB, dayC, dayD, start_cur_diffSecond, cur_end_diffSecond, claimedAmount; - uint32 curDate, tmpDate, numberOfInvestedProjects; - sint32 i, j, k; - uint8 curVestingStep, vestingPercent; - bit flag; - }; + return privateAuctionFee >= 0 && publicAuctionCreationFee >= 0 && auctionCancellationFeeBasisPoints <= NOST_BASIS_POINTS_SCALE && + managementFeeBasisPoints <= NOST_BASIS_POINTS_SCALE && developmentFeeBasisPoints <= NOST_BASIS_POINTS_SCALE && + takeoverCoordinatorFeeBasisPoints <= NOST_BASIS_POINTS_SCALE && shareholderDividendBasisPoints <= NOST_BASIS_POINTS_SCALE && + shareholderFeeBasisPointsTier1 <= NOST_BASIS_POINTS_SCALE && shareholderFeeBasisPointsTier2 <= NOST_BASIS_POINTS_SCALE && + shareholderFeeBasisPointsTier3 <= NOST_BASIS_POINTS_SCALE && shareholderFeeBasisPointsTier4 <= NOST_BASIS_POINTS_SCALE && + (shareholderFeeBasisPointsTier1 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= + NOST_BASIS_POINTS_SCALE && + (shareholderFeeBasisPointsTier2 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= + NOST_BASIS_POINTS_SCALE && + (shareholderFeeBasisPointsTier3 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= + NOST_BASIS_POINTS_SCALE && + (shareholderFeeBasisPointsTier4 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= + NOST_BASIS_POINTS_SCALE; + } - PUBLIC_FUNCTION_WITH_LOCALS(getMaxClaimAmount) + /** + * @brief Selects the shareholder fee tier for a gross auction amount. + */ + static uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount, const StateData& state) { - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); - - if (input.indexOfFundraising >= state.get().numberOfFundraising) + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1) { - return ; + return state.shareholderFeeBasisPointsTier1; } - - state.get().investors.get(input.investorId, locals.tmpInvestedList); - if (state.get().numberOfInvestedProjects.get(input.investorId, locals.numberOfInvestedProjects) == 0) + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2) { - return ; + return state.shareholderFeeBasisPointsTier2; } - - for (locals.i = 0; locals.i < (sint32)locals.numberOfInvestedProjects; locals.i++) + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3) { - if (locals.tmpInvestedList.get(locals.i).indexOfFundraising == input.indexOfFundraising) - { - locals.investedAmount = locals.tmpInvestedList.get(locals.i).investedAmount; - locals.claimedAmount = locals.tmpInvestedList.get(locals.i).claimedAmount; - locals.tmpInvestData = locals.tmpInvestedList.get(locals.i); - break; - } + return state.shareholderFeeBasisPointsTier3; } + return state.shareholderFeeBasisPointsTier4; + } - if (locals.i == locals.numberOfInvestedProjects) - { - return ; - } + /** + * @brief Selects the shareholder fee tier from contract state. + */ + static uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount, const ContractState& state) + { + return getAuctionShareholderFeeBasisPoints(grossAmount, state.get()); + } - if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).listingStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate) + /** @brief Returns the zero-based shareholder fee tier selected by an auction gross amount. */ + constexpr static uint64 getAuctionShareholderFeeTierIndex(uint64 grossAmount) + { + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1) { - output.amount = div(div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice) * state.get().fundaraisings.get(input.indexOfFundraising).TGE, 100ULL); + return 0; } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate) + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2) { - locals.tmpDate = state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate; - diffDateInSecond(locals.tmpDate, locals.curDate, locals.j, locals.dayA, locals.dayB, locals.start_cur_diffSecond); - locals.tmpDate = state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate; - diffDateInSecond(locals.curDate, locals.tmpDate, locals.k, locals.dayC, locals.dayD, locals.cur_end_diffSecond); - - locals.curVestingStep = (uint8)div(locals.start_cur_diffSecond, div(locals.start_cur_diffSecond + locals.cur_end_diffSecond, state.get().fundaraisings.get(input.indexOfFundraising).stepOfVesting * 1ULL)) + 1; - locals.vestingPercent = (uint8)div(100ULL - state.get().fundaraisings.get(input.indexOfFundraising).TGE, state.get().fundaraisings.get(input.indexOfFundraising).stepOfVesting * 1ULL) * locals.curVestingStep; - output.amount = div(div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice) * (state.get().fundaraisings.get(input.indexOfFundraising).TGE + locals.vestingPercent), 100ULL); + return 1; } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate) + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3) { - output.amount = div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice); + return 2; } + return 3; } - REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + /** @brief Returns the saturating aggregate of every unsettled fee-pool accumulator. */ + static uint64 getNostromoFeePoolTotal(const NostromoFeePool& feePool) { - REGISTER_USER_FUNCTION(getStats, 1); - REGISTER_USER_FUNCTION(getTierLevelByUser, 2); - REGISTER_USER_FUNCTION(getUserVoteStatus, 3); - REGISTER_USER_FUNCTION(checkTokenCreatability, 4); - REGISTER_USER_FUNCTION(getNumberOfInvestedProjects, 5); - REGISTER_USER_FUNCTION(getProjectByIndex, 6); - REGISTER_USER_FUNCTION(getFundarasingByIndex, 7); - REGISTER_USER_FUNCTION(getProjectIndexListByCreator, 8); - REGISTER_USER_FUNCTION(getInfoUserInvested, 9); - REGISTER_USER_FUNCTION(getMaxClaimAmount, 10); - - REGISTER_USER_PROCEDURE(registerInTier, 1); - REGISTER_USER_PROCEDURE(logoutFromTier, 2); - REGISTER_USER_PROCEDURE(createProject, 3); - REGISTER_USER_PROCEDURE(voteInProject, 4); - REGISTER_USER_PROCEDURE(createFundraising, 5); - REGISTER_USER_PROCEDURE(investInProject, 6); - REGISTER_USER_PROCEDURE(claimToken, 7); - REGISTER_USER_PROCEDURE(upgradeTier, 8); - REGISTER_USER_PROCEDURE(TransferShareManagementRights, 9); + return sadd(sadd(sadd(feePool.shareholderDividendTier1Amount, feePool.shareholderDividendTier2Amount), + sadd(feePool.shareholderDividendTier3Amount, feePool.shareholderDividendTier4Amount)), + sadd(sadd(feePool.commonServiceFeeAmount, feePool.shareholderDividendAmount), + sadd(sadd(feePool.managementAmount, feePool.developmentAmount), feePool.takeoverCoordinatorAmount))); } - INITIALIZE() + /** + * @brief Computes `floor(amount * basisPoints / 10000)` without overflowing the intermediate product. + */ + static uint64 calculateBasisPointAmount(uint64 amount, uint64 basisPoints) { - state.mut().teamAddress = ID(_G, _E, _H, _N, _R, _F, _U, _O, _I, _I, _C, _S, _B, _C, _S, _R, _F, _M, _N, _J, _T, _C, _J, _K, _C, _J, _H, _A, _T, _Z, _X, _A, _X, _Y, _O, _F, _W, _X, _U, _F, _L, _C, _K, _F, _P, _B, _W, _X, _Q, _A, _C, _B, _S, _Z, _F, _F); - state.mut().transferRightsFee = 100; + return sadd(smul(div(amount, NOST_BASIS_POINTS_SCALE), basisPoints), + div(smul(mod(amount, NOST_BASIS_POINTS_SCALE), basisPoints), NOST_BASIS_POINTS_SCALE)); } - struct END_EPOCH_locals + /** + * @brief Computes the exact auction fee split without performing transfers. + * @note Keep this helper pure so tests can reuse the same arithmetic as `DistributeAuctionRevenue`. + */ + static void calculateAuctionRevenueBreakdown(uint64 grossAmount, const ContractState& state, + AuctionRevenueBreakdown& output) { - fundaraisingInfo tmpFundraising; - investInfo tmpInvest; - Array votedList; - Array clearedVotedList; - id userId; - sint64 idx; - uint32 numberOfVotedProject, clearedNumberOfVotedProject, i, j, curDate, indexOfProject, numberOfInvestedProjects, tierLevel; - }; + output.sellerPayout = grossAmount; + output.shareholderFeeBasisPoints = getAuctionShareholderFeeBasisPoints(grossAmount, state); + output.shareholderFeeAmount = calculateBasisPointAmount(grossAmount, output.shareholderFeeBasisPoints); + output.shareholderDividendAmount = calculateBasisPointAmount(output.shareholderFeeAmount, state.get().shareholderDividendBasisPoints); + output.managementFeeAmount = calculateBasisPointAmount(grossAmount, state.get().managementFeeBasisPoints); + output.developmentFeeAmount = calculateBasisPointAmount(grossAmount, state.get().developmentFeeBasisPoints); + output.takeoverCoordinatorBaseAmount = calculateBasisPointAmount(grossAmount, state.get().takeoverCoordinatorFeeBasisPoints); + output.takeoverCoordinatorFeeAmount = output.takeoverCoordinatorBaseAmount + (output.shareholderFeeAmount - output.shareholderDividendAmount); + output.sellerPayout = grossAmount - output.shareholderFeeAmount - output.managementFeeAmount - output.developmentFeeAmount - + output.takeoverCoordinatorBaseAmount; + } - END_EPOCH_WITH_LOCALS() + /** + * @brief Computes the exact service-fee split without performing transfers. + * @note Keep this helper pure so tests can reuse the same arithmetic as `DistributeNostromoFeePool`. + */ + static void calculateAuctionServiceFeeBreakdown(uint64 feeAmount, AuctionServiceFeeBreakdown& output) { - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); + output.shareholderDividendAmount = calculateBasisPointAmount(feeAmount, NOST_AUCTION_SERVICE_FEE_SHAREHOLDER_BP); + output.managementFeeAmount = calculateBasisPointAmount(feeAmount, NOST_AUCTION_SERVICE_FEE_MANAGEMENT_BP); + output.developmentFeeAmount = calculateBasisPointAmount(feeAmount, NOST_AUCTION_SERVICE_FEE_DEVELOPMENT_BP); + output.takeoverCoordinatorFeeAmount = calculateBasisPointAmount(feeAmount, NOST_AUCTION_SERVICE_FEE_TAKEOVER_COORDINATOR_BP); + // Shareholders receive the rounding remainder so the entire collected fee is distributed on-chain. + output.shareholderDividendAmount = + sadd(output.shareholderDividendAmount, feeAmount - output.shareholderDividendAmount - output.managementFeeAmount - + output.developmentFeeAmount - output.takeoverCoordinatorFeeAmount); + } - locals.idx = state.get().investors.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) + /** + * @brief Computes escrow, bid fee, and required reward for a Batch Auction bid. + */ + static void calculateBatchAuctionBidFee(uint64 bidQuantity, uint64 bidAmount, CalculateBatchAuctionBidFee_output& output) + { + output.escrowAmount = smul(bidQuantity, bidAmount); + if (output.escrowAmount == 0) { - locals.userId = state.get().investors.key(locals.idx); - state.get().investors.get(locals.userId, state.mut().tmpInvestedList); - state.get().numberOfInvestedProjects.get(locals.userId, locals.numberOfInvestedProjects); - - for (locals.i = 0; locals.i < locals.numberOfInvestedProjects; locals.i++) - { - if (state.get().fundaraisings.get(locals.i).thirdPhaseEndDate < locals.curDate && state.get().fundaraisings.get(locals.i).isCreatedToken == 0 && state.get().fundaraisings.get(locals.i).raisedFunds != 0) - { - qpi.transfer(locals.userId, state.get().tmpInvestedList.get(locals.i).investedAmount); - state.mut().tmpInvestedList.set(locals.i, state.get().tmpInvestedList.get(--locals.numberOfInvestedProjects)); - } - } - if (locals.numberOfInvestedProjects == 0) - { - state.mut().investors.removeByKey(locals.userId); - state.mut().numberOfInvestedProjects.removeByKey(locals.userId); - } - else - { - state.mut().investors.set(locals.userId, state.get().tmpInvestedList); - state.mut().numberOfInvestedProjects.set(locals.userId, locals.numberOfInvestedProjects); - } - locals.idx = state.get().investors.nextElementIndex(locals.idx); + output.fee = 0; + output.requiredReward = 0; + return; } - for (locals.i = 0; locals.i < state.get().numberOfFundraising; locals.i++) - { - if (state.get().fundaraisings.get(locals.i).thirdPhaseEndDate < locals.curDate && state.get().fundaraisings.get(locals.i).isCreatedToken == 0 && state.get().fundaraisings.get(locals.i).raisedFunds != 0) - { - locals.tmpFundraising = state.get().fundaraisings.get(locals.i); - locals.tmpFundraising.raisedFunds = 0; - state.mut().fundaraisings.set(locals.i, locals.tmpFundraising); - } - else if (state.get().fundaraisings.get(locals.i).thirdPhaseEndDate < locals.curDate && state.get().fundaraisings.get(locals.i).isCreatedToken == 1 && state.get().fundaraisings.get(locals.i).raisedFunds != 0) - { - locals.tmpFundraising = state.get().fundaraisings.get(locals.i); + output.fee = output.escrowAmount <= NOST_BATCH_BID_FEE_CUTOFF ? NOST_BATCH_BID_FEE_CUTOFF - output.escrowAmount : 0; + output.requiredReward = sadd(output.escrowAmount, output.fee); + } - state.mut().epochRevenue += div(locals.tmpFundraising.raisedFunds * 5, 100ULL); - qpi.transfer(state.get().projects.get(locals.tmpFundraising.indexOfProject).creator, locals.tmpFundraising.raisedFunds - div(locals.tmpFundraising.raisedFunds * 5, 100ULL)); + /** + * @brief Returns the service fee required to create an auction. + */ + static sint64 getCreateAuctionFee(EAuctionVisibility visibility, const ContractState& state) + { + switch (visibility) + { + case EAuctionVisibility::Public: return state.get().publicAuctionCreationFee; break; + case EAuctionVisibility::Private: return state.get().privateAuctionFee; break; + default: break; + } + return 0; + } - qpi.transferShareOwnershipAndPossession(state.get().projects.get(locals.tmpFundraising.indexOfProject).tokenName, SELF, SELF, SELF, state.get().fundaraisings.get(locals.i).soldAmount - div(locals.tmpFundraising.raisedFunds, state.get().fundaraisings.get(locals.i).tokenPrice), state.get().projects.get(locals.tmpFundraising.indexOfProject).creator); + /** + * @brief Returns whether an auction type is accepted by the contract. + */ + static bool isSupportedAuctionType(EAuctionType auctionType) + { + return auctionType == EAuctionType::Batch || auctionType == EAuctionType::Standard; + } - locals.tmpFundraising.raisedFunds = 0; - state.mut().fundaraisings.set(locals.i, locals.tmpFundraising); - } - } + /** + * @brief Returns whether an auction visibility is accepted by the contract. + */ + static bool isSupportedAuctionVisibility(EAuctionVisibility visibility) + { + return visibility == EAuctionVisibility::Public || visibility == EAuctionVisibility::Private; + } - qpi.transfer(state.get().teamAddress, div(state.get().epochRevenue, 10ULL)); - state.mut().epochRevenue -= div(state.get().epochRevenue, 10ULL); - qpi.distributeDividends(div(state.get().epochRevenue, 676ULL)); - state.mut().epochRevenue -= div(state.get().epochRevenue, 676ULL) * 676; + /** + * @brief Returns whether an asset entry is empty. + */ + static bool isZeroAsset(const Asset& asset) { return asset.assetName == 0 && isZero(asset.issuer); } - locals.idx = state.get().users.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) - { - locals.userId = state.get().users.key(locals.idx); - locals.tierLevel = state.get().users.value(locals.idx); + /** @brief Returns whether the runtime fee override routes every auction fee to the development wallet. */ + static bool routeAllFeesToDevelopment(const QPI::ContractState& state) + { + return state.get().routeAllFeesToDevelopment; + } - if (state.get().numberOfVotedProject.get(locals.userId, locals.numberOfVotedProject)) - { - state.get().voteStatus.get(locals.userId, locals.votedList); - locals.clearedNumberOfVotedProject = 0; - for (locals.j = 0; locals.j < locals.numberOfVotedProject; locals.j++) - { - locals.indexOfProject = locals.votedList.get(locals.j); + /** + * @brief Packs year, month, and day into the contract date-stamp format. + */ + static void makeDateStamp(uint8 year, uint8 month, uint8 day, uint32& res) + { + res = static_cast(year << NOST_DATE_STAMP_YEAR_SHIFT | month << NOST_DATE_STAMP_MONTH_SHIFT | day); + } - if (state.get().projects.get(locals.indexOfProject).endDate > locals.curDate) - { - locals.clearedVotedList.set(locals.clearedNumberOfVotedProject++, locals.indexOfProject); - } - } - if (locals.clearedNumberOfVotedProject == 0) - { - state.mut().numberOfVotedProject.removeByKey(locals.userId); - state.mut().voteStatus.removeByKey(locals.userId); - } - else - { - state.mut().numberOfVotedProject.set(locals.userId, locals.clearedNumberOfVotedProject); - state.mut().voteStatus.set(locals.userId, locals.clearedVotedList); - } - } + /** + * @brief Expands an accumulated pause window to include a candidate window. + */ + static void accumulatePauseWindow(uint8& hasPauseWindow, DateAndTime& pauseStartedAt, DateAndTime& pauseEndsAt, + const DateAndTime& candidatePauseStartedAt, const DateAndTime& candidatePauseEndsAt) + { + if (!hasPauseWindow) + { + hasPauseWindow = 1; + pauseStartedAt = candidatePauseStartedAt; + pauseEndsAt = candidatePauseEndsAt; + return; + } - locals.idx = state.get().users.nextElementIndex(locals.idx); + if (candidatePauseStartedAt < pauseStartedAt) + { + pauseStartedAt = candidatePauseStartedAt; } + if (candidatePauseEndsAt > pauseEndsAt) + { + pauseEndsAt = candidatePauseEndsAt; + } + } - if (state.get().users.needsCleanup()) { state.mut().users.cleanup(); } - if (state.get().investors.needsCleanup()) { state.mut().investors.cleanup(); } - if (state.get().numberOfInvestedProjects.needsCleanup()) { state.mut().numberOfInvestedProjects.cleanup(); } - if (state.get().numberOfVotedProject.needsCleanup()) { state.mut().numberOfVotedProject.cleanup(); } - if (state.get().voteStatus.needsCleanup()) { state.mut().voteStatus.cleanup(); } + /** + * @brief Compares two Nostromo timestamps. + * @param a Left-hand date-time. + * @param b Right-hand date-time. + * @return `-1` if `a < b`, `0` if `a == b`, `1` if `a > b`. + */ + static sint32 dateCompare(const DateAndTime& a, const DateAndTime& b) + { + if (a < b) + { + return -1; + } + if (a > b) + { + return 1; + } + return 0; } - PRE_ACQUIRE_SHARES() - { - output.allowTransfer = true; - } + /** + * @brief Computes the difference in seconds between two `DateAndTime` values. + * @param a Start date-time. + * @param b End date-time. + * @param res Output difference in seconds, or `0` when `A >= B`. + */ + static void diffDateInSecond(const DateAndTime& a, const DateAndTime& b, uint64& res) + { + if (a >= b) + { + res = 0; + return; + } + res = div(a.durationMicrosec(b), NOST_MICROSECONDS_PER_SECOND); + } }; diff --git a/src/qpi/impl/qpi_system_impl.h b/src/qpi/impl/qpi_system_impl.h index 383dacad..bb98845d 100644 --- a/src/qpi/impl/qpi_system_impl.h +++ b/src/qpi/impl/qpi_system_impl.h @@ -1,14 +1,19 @@ -#pragma once - -#include "qpi/qpi.h" -#include "system.h" - -unsigned short QPI::QpiContextFunctionCall::epoch() const -{ - return system.epoch; -} - -unsigned int QPI::QpiContextFunctionCall::tick() const -{ - return system.tick; -} +#pragma once + +#include "qpi/qpi.h" +#include "system.h" + +unsigned short QPI::QpiContextFunctionCall::epoch() const +{ + return system.epoch; +} + +unsigned int QPI::QpiContextFunctionCall::tick() const +{ + return system.tick; +} + +unsigned int QPI::QpiContextFunctionCall::initialTick() const +{ + return system.initialTick; +} diff --git a/src/qpi/qpi_context.h b/src/qpi/qpi_context.h index 07854a4d..032e79af 100644 --- a/src/qpi/qpi_context.h +++ b/src/qpi/qpi_context.h @@ -185,6 +185,9 @@ namespace QPI inline uint32 tick( ) const; // [0..999'999'999] + inline uint32 initialTick( + ) const; + inline uint8 year( ) const; // [0..99] (0 = 2000, 1 = 2001, ..., 99 = 2099) diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index 6f9c8e67..a16d2233 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -1,1692 +1,4011 @@ #define NO_UEFI -#include -#include - #include "contract_testing.h" -static std::mt19937_64 rand64; +using namespace QPI; + +namespace +{ + static constexpr uint64 QX_ISSUE_ASSET_FEE = 1000000000ULL; + static constexpr uint64 QX_TRANSFER_ASSET_FEE = 1000000ULL; + static const id NOST_CONTRACT_ID(NOST_CONTRACT_INDEX, 0, 0, 0); +} // namespace + +class ContractTestingNOST : protected ContractTesting +{ +public: + ContractTestingNOST() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(NOST); + system.initialTick = system.tick; + system.epoch = contractDescriptions[NOST_CONTRACT_INDEX].constructionEpoch + 10; + callSystemProcedure(NOST_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QX); + callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); + setNow(2026, 1, 1, 9, 0, 0); + callSystemProcedure(NOST_CONTRACT_INDEX, END_TICK); + } + + void ensureUser(const id& user, sint64 amount = 1000) + { + if (getBalance(user) == 0) + { + increaseEnergy(user, amount); + } + } + + void seedUser(const id& user, sint64 amount = 2000000000LL) { increaseEnergy(user, amount); } + + void setNow(uint16 year, uint8 month, uint8 day, uint8 hour, uint8 minute, uint8 second) + { + utcTime.Year = year; + utcTime.Month = month; + utcTime.Day = day; + utcTime.Hour = hour; + utcTime.Minute = minute; + utcTime.Second = second; + utcTime.Nanosecond = 0; + updateQpiTime(); + } + + void advanceAndEndTick(uint64 milliseconds) + { + advanceTimeAndTick(milliseconds); + callSystemProcedure(NOST_CONTRACT_INDEX, END_TICK); + } + + void advanceTicks(uint32 count, uint64 millisecondsPerTick = 1000ULL) + { + for (uint32 i = 0; i < count; ++i) + { + advanceAndEndTick(millisecondsPerTick); + } + } + + void beginEpoch() + { + system.initialTick = system.tick; + ++system.epoch; + callSystemProcedure(NOST_CONTRACT_INDEX, BEGIN_EPOCH); + } + + void endEpoch() { callSystemProcedure(NOST_CONTRACT_INDEX, END_EPOCH); } + + sint64 issueAsset(const id& issuer, uint64 assetName, sint64 numberOfShares) + { + QX::IssueAsset_input input{}; + QX::IssueAsset_output output{}; + + input.assetName = assetName; + input.numberOfShares = numberOfShares; + input.unitOfMeasurement = 0; + input.numberOfDecimalPlaces = 0; + + seedUser(issuer, QX_ISSUE_ASSET_FEE); + invokeUserProcedure(QX_CONTRACT_INDEX, 1, input, output, issuer, QX_ISSUE_ASSET_FEE); + return output.issuedNumberOfShares; + } + + sint64 transferAsset(const id& owner, const id& recipient, const Asset& asset, sint64 numberOfShares) + { + QX::TransferShareOwnershipAndPossession_input input{}; + QX::TransferShareOwnershipAndPossession_output output{}; + + input.issuer = asset.issuer; + input.newOwnerAndPossessor = recipient; + input.assetName = asset.assetName; + input.numberOfShares = numberOfShares; + + seedUser(owner, QX_TRANSFER_ASSET_FEE); + invokeUserProcedure(QX_CONTRACT_INDEX, 2, input, output, owner, QX_TRANSFER_ASSET_FEE); + return output.transferredNumberOfShares; + } + + sint64 transferShareManagementRightsToNostromo(const id& owner, const Asset& asset, sint64 numberOfShares) + { + QX::TransferShareManagementRights_input input{}; + QX::TransferShareManagementRights_output output{}; + + input.asset = asset; + input.numberOfShares = numberOfShares; + input.newManagingContractIndex = NOST_CONTRACT_INDEX; + + invokeUserProcedure(QX_CONTRACT_INDEX, 9, input, output, owner, 0); + return output.transferredNumberOfShares; + } + + NOST::CreateAuction_output createAuction(const id& seller, const NOST::CreateAuction_input& input, + sint64 reward = NOST_PUBLIC_AUCTION_CREATION_FEE) + { + if (reward > 0) + { + seedUser(seller, reward); + } + else + { + ensureUser(seller); + } + return createAuctionWithFundedReward(seller, input, reward); + } + + NOST::CreateAuction_output createAuctionWithFundedReward(const id& seller, const NOST::CreateAuction_input& input, sint64 reward) + { + NOST::CreateAuction_output output{}; + invokeUserProcedure(NOST_CONTRACT_INDEX, 1, input, output, seller, reward); + return output; + } + + NOST::PlaceBid_output placeBid(const id& bidder, uint64 auctionIndex, uint64 quantity, uint64 bidAmount, sint64 reward) + { + NOST::PlaceBid_input input{}; + NOST::PlaceBid_output output{}; + + input.auctionIndex = auctionIndex; + input.quantity = quantity; + input.bidAmount = bidAmount; + + seedUser(bidder, reward); + invokeUserProcedure(NOST_CONTRACT_INDEX, 2, input, output, bidder, reward); + return output; + } + + NOST::PlaceBid_output placeBidWithFundedReward(const id& bidder, uint64 auctionIndex, uint64 quantity, uint64 bidAmount, sint64 reward) + { + NOST::PlaceBid_input input{}; + NOST::PlaceBid_output output{}; + + input.auctionIndex = auctionIndex; + input.quantity = quantity; + input.bidAmount = bidAmount; + + invokeUserProcedure(NOST_CONTRACT_INDEX, 2, input, output, bidder, reward); + return output; + } + + NOST::PlaceBid_output placeBatchBidWithRequiredReward(const id& bidder, uint64 auctionIndex, uint64 bidQuantity, uint64 bidAmount) + { + const NOST::CalculateBatchAuctionBidFee_output& calculation = calculateBatchAuctionBidFee(bidQuantity, bidAmount); + return placeBid(bidder, auctionIndex, bidQuantity, bidAmount, static_cast(calculation.requiredReward)); + } + + NOST::PlaceBid_output placeBatchBidWithFundedRequiredReward(const id& bidder, uint64 auctionIndex, uint64 bidQuantity, uint64 bidAmount) + { + const NOST::CalculateBatchAuctionBidFee_output& calculation = calculateBatchAuctionBidFee(bidQuantity, bidAmount); + return placeBidWithFundedReward(bidder, auctionIndex, bidQuantity, bidAmount, static_cast(calculation.requiredReward)); + } + + NOST::CancelAuction_output cancelAuction(const id& seller, uint64 auctionIndex, sint64 reward) + { + NOST::CancelAuction_input input{}; + NOST::CancelAuction_output output{}; + + input.auctionIndex = auctionIndex; + if (reward > 0) + { + seedUser(seller, reward); + } + else + { + ensureUser(seller); + } + invokeUserProcedure(NOST_CONTRACT_INDEX, 3, input, output, seller, reward); + return output; + } + + NOST::TransferShareManagementRights_output transferManagedSharesWithReward(const id& owner, const Asset& asset, sint64 numberOfShares, + uint32 contractIndex, sint64 reward) + { + if (reward > 0) + { + seedUser(owner, reward); + } + else + { + ensureUser(owner); + } + return transferManagedSharesWithFundedReward(owner, asset, numberOfShares, contractIndex, reward); + } + + NOST::TransferShareManagementRights_output transferManagedSharesWithFundedReward(const id& owner, const Asset& asset, sint64 numberOfShares, + uint32 contractIndex, sint64 reward) + { + NOST::TransferShareManagementRights_input input{}; + NOST::TransferShareManagementRights_output output{}; + + input.asset = asset; + input.numberOfShares = numberOfShares; + input.newManagingContractIndex = contractIndex; + + invokeUserProcedure(NOST_CONTRACT_INDEX, 4, input, output, owner, reward); + return output; + } + + NOST::TransferShareManagementRights_output transferManagedShares(const id& owner, const Asset& asset, sint64 numberOfShares, uint32 contractIndex) + { + syncCachedQxTransferFee(); + return transferManagedSharesWithReward(owner, asset, numberOfShares, contractIndex, getCachedQxTransferFee()); + } + + NOST::ResolvePendingStandardAuction_output resolvePendingStandardAuction(const id& seller, uint64 auctionIndex, bool acceptSale) + { + NOST::ResolvePendingStandardAuction_input input{}; + NOST::ResolvePendingStandardAuction_output output{}; + + input.auctionIndex = auctionIndex; + input.acceptSale = acceptSale ? 1 : 0; + + ensureUser(seller); + invokeUserProcedure(NOST_CONTRACT_INDEX, 5, input, output, seller, 0); + return output; + } + + NOST::SetAuctionFees_output setAuctionFees(const id& caller, const NOST::SetAuctionFees_input& input) + { + NOST::SetAuctionFees_output output{}; + ensureUser(caller); + invokeUserProcedure(NOST_CONTRACT_INDEX, 6, input, output, caller, 0); + return output; + } + + NOST::SetAuctionFeesByManagement_output setAuctionFeesByManagement(const id& caller, const NOST::SetAuctionFeesByManagement_input& input) + { + NOST::SetAuctionFeesByManagement_output output{}; + ensureUser(caller); + invokeUserProcedure(NOST_CONTRACT_INDEX, 7, input, output, caller, 0); + return output; + } + + NOST::SetManagement_output setManagement(const id& caller, const id& management) + { + NOST::SetManagement_input input{}; + NOST::SetManagement_output output{}; + + input.management = management; + ensureUser(caller); + invokeUserProcedure(NOST_CONTRACT_INDEX, 8, input, output, caller, 0); + return output; + } + + NOST::GetAuctionByIndex_output getAuction(uint64 auctionIndex) const + { + NOST::GetAuctionByIndex_input input{}; + NOST::GetAuctionByIndex_output output{}; + + input.auctionIndex = auctionIndex; + callFunction(NOST_CONTRACT_INDEX, 1, input, output); + return output; + } + + NOST::GetAuctionParticipant_output getParticipant(uint64 auctionIndex, const id& participant) const + { + NOST::GetAuctionParticipant_input input{}; + NOST::GetAuctionParticipant_output output{}; + + input.auctionIndex = auctionIndex; + input.participant = participant; + callFunction(NOST_CONTRACT_INDEX, 2, input, output); + return output; + } + + NOST::GetTicksBeforeAuctionLaunch_output getTicksBeforeAuctionLaunch() const + { + NOST::GetTicksBeforeAuctionLaunch_input input{}; + NOST::GetTicksBeforeAuctionLaunch_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 3, input, output); + return output; + } + + NOST::GetAuctionFees_output getAuctionFees() const + { + NOST::GetAuctionFees_input input{}; + NOST::GetAuctionFees_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 4, input, output); + return output; + } + + NOST::SetAuctionFees_input makeCoordinatorFeeInput(sint64 publicAuctionCreationFee) const + { + const NOST::GetAuctionFees_output& fees = getAuctionFees(); + NOST::SetAuctionFees_input input{}; + input.privateAuctionFee = fees.privateAuctionFee; + input.publicAuctionCreationFee = publicAuctionCreationFee; + input.auctionCancellationFeeBasisPoints = fees.auctionCancellationFeeBasisPoints; + input.managementFeeBasisPoints = fees.managementFeeBasisPoints; + input.developmentFeeBasisPoints = fees.developmentFeeBasisPoints; + input.takeoverCoordinatorFeeBasisPoints = fees.takeoverCoordinatorFeeBasisPoints; + input.shareholderDividendBasisPoints = fees.shareholderDividendBasisPoints; + input.shareholderFeeBasisPointsTier1 = fees.shareholderFeeBasisPointsTier1; + input.shareholderFeeBasisPointsTier2 = fees.shareholderFeeBasisPointsTier2; + input.shareholderFeeBasisPointsTier3 = fees.shareholderFeeBasisPointsTier3; + input.shareholderFeeBasisPointsTier4 = fees.shareholderFeeBasisPointsTier4; + return input; + } + + NOST::SetAuctionFeesByManagement_input makeManagementFeeInput(sint64 publicAuctionCreationFee) const + { + const NOST::GetAuctionFees_output& fees = getAuctionFees(); + NOST::SetAuctionFeesByManagement_input input{}; + input.privateAuctionFee = fees.privateAuctionFee; + input.publicAuctionCreationFee = publicAuctionCreationFee; + input.auctionCancellationFeeBasisPoints = fees.auctionCancellationFeeBasisPoints; + input.managementFeeBasisPoints = fees.managementFeeBasisPoints; + input.developmentFeeBasisPoints = fees.developmentFeeBasisPoints; + input.shareholderFeeBasisPointsTier1 = fees.shareholderFeeBasisPointsTier1; + input.shareholderFeeBasisPointsTier2 = fees.shareholderFeeBasisPointsTier2; + input.shareholderFeeBasisPointsTier3 = fees.shareholderFeeBasisPointsTier3; + input.shareholderFeeBasisPointsTier4 = fees.shareholderFeeBasisPointsTier4; + return input; + } + + NOST::CalculateBatchAuctionBidFee_output calculateBatchAuctionBidFee(uint64 bidQuantity, uint64 bidAmount) const + { + NOST::CalculateBatchAuctionBidFee_input input{}; + NOST::CalculateBatchAuctionBidFee_output output{}; + + input.bidQuantity = bidQuantity; + input.bidAmount = bidAmount; + callFunction(NOST_CONTRACT_INDEX, 20, input, output); + return output; + } + + NOST::GetFeeRecipients_output getFeeRecipients() const + { + NOST::GetFeeRecipients_input input{}; + NOST::GetFeeRecipients_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 5, input, output); + return output; + } + + NOST::GetClosedAuctionHistory_output getClosedAuctionHistory() const + { + NOST::GetClosedAuctionHistory_input input{}; + NOST::GetClosedAuctionHistory_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 6, input, output); + return output; + } + + NOST::GetRouteAllFeesToDevelopment_output getRouteAllFeesToDevelopmentPublic() const + { + NOST::GetRouteAllFeesToDevelopment_input input{}; + NOST::GetRouteAllFeesToDevelopment_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 7, input, output); + return output; + } + + NOST::GetContractStats_output getContractStats() const + { + NOST::GetContractStats_input input{}; + NOST::GetContractStats_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 8, input, output); + return output; + } + + NOST::GetAuctionSummaries_output getAuctionSummaries(uint64 offset, uint64 limit) const + { + NOST::GetAuctionSummaries_input input{}; + NOST::GetAuctionSummaries_output output{}; + + input.offset = offset; + input.limit = limit; + callFunction(NOST_CONTRACT_INDEX, 9, input, output); + return output; + } + + NOST::GetActiveAuctionIndices_output getActiveAuctionIndices(uint64 offset, uint64 limit) const + { + NOST::GetActiveAuctionIndices_input input{}; + NOST::GetActiveAuctionIndices_output output{}; + + input.offset = offset; + input.limit = limit; + callFunction(NOST_CONTRACT_INDEX, 10, input, output); + return output; + } + + NOST::GetAuctionsBySeller_output getAuctionsBySeller(const id& seller, uint64 offset, uint64 limit) const + { + NOST::GetAuctionsBySeller_input input{}; + NOST::GetAuctionsBySeller_output output{}; + + input.seller = seller; + input.offset = offset; + input.limit = limit; + callFunction(NOST_CONTRACT_INDEX, 11, input, output); + return output; + } + + NOST::GetAuctionByMetadataCid_output getAuctionByMetadataCid(const Array& metadataCid) const + { + NOST::GetAuctionByMetadataCid_input input{}; + NOST::GetAuctionByMetadataCid_output output{}; + + input.metadataIpfsCid = metadataCid; + callFunction(NOST_CONTRACT_INDEX, 12, input, output); + return output; + } + + NOST::GetAuctionSummariesByIndexBatch_output getAuctionSummariesByIndexBatch(const Array& auctionIndices, + uint64 count) const + { + NOST::GetAuctionSummariesByIndexBatch_input input{}; + NOST::GetAuctionSummariesByIndexBatch_output output{}; + + input.auctionIndices = auctionIndices; + input.count = count; + callFunction(NOST_CONTRACT_INDEX, 13, input, output); + return output; + } + + NOST::GetAuctionParticipants_output getAuctionParticipants(uint64 auctionIndex, uint64 offset, uint64 limit) const + { + NOST::GetAuctionParticipants_input input{}; + NOST::GetAuctionParticipants_output output{}; + + input.auctionIndex = auctionIndex; + input.offset = offset; + input.limit = limit; + callFunction(NOST_CONTRACT_INDEX, 14, input, output); + return output; + } + + NOST::GetUserParticipations_output getUserParticipations(const id& participant, uint64 offset, uint64 limit) const + { + NOST::GetUserParticipations_input input{}; + NOST::GetUserParticipations_output output{}; + + input.participant = participant; + input.offset = offset; + input.limit = limit; + callFunction(NOST_CONTRACT_INDEX, 15, input, output); + return output; + } + + NOST::GetLatestAuctionIndex_output getLatestAuctionIndex() const + { + NOST::GetLatestAuctionIndex_input input{}; + NOST::GetLatestAuctionIndex_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 16, input, output); + return output; + } + + NOST::GetAuctionCountBySeller_output getAuctionCountBySeller(const id& seller) const + { + NOST::GetAuctionCountBySeller_input input{}; + NOST::GetAuctionCountBySeller_output output{}; + + input.seller = seller; + callFunction(NOST_CONTRACT_INDEX, 17, input, output); + return output; + } + + NOST::GetAuctionAtCreationSnapshot_output getAuctionAtCreationSnapshot(uint64 auctionIndex) const + { + NOST::GetAuctionAtCreationSnapshot_input input{}; + NOST::GetAuctionAtCreationSnapshot_output output{}; + + input.auctionIndex = auctionIndex; + callFunction(NOST_CONTRACT_INDEX, 18, input, output); + return output; + } + + NOST::GetBatchAuctionBidAvailability_output getBatchAvailability(uint64 auctionIndex) const + { + NOST::GetBatchAuctionBidAvailability_input input{}; + NOST::GetBatchAuctionBidAvailability_output output{}; + + input.auctionIndex = auctionIndex; + callFunction(NOST_CONTRACT_INDEX, 19, input, output); + return output; + } + + NOST::GetPendingServiceFeePool_output getPendingServiceFeePool() const + { + NOST::GetPendingServiceFeePool_input input{}; + NOST::GetPendingServiceFeePool_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 21, input, output); + return output; + } + + NOST::GetFeeReserveGuardState_output getFeeReserveGuardState() const + { + NOST::GetFeeReserveGuardState_input input{}; + NOST::GetFeeReserveGuardState_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 22, input, output); + return output; + } + + NOST::GetPendingPayout_output getPendingPayout(const id& account) const + { + NOST::GetPendingPayout_input input{}; + NOST::GetPendingPayout_output output{}; + input.account = account; + callFunction(NOST_CONTRACT_INDEX, 23, input, output); + return output; + } + + NOST::GetNostromoFeePool_output getNostromoFeePool() const + { + NOST::GetNostromoFeePool_input input{}; + NOST::GetNostromoFeePool_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 24, input, output); + return output; + } + + NOST::SetFeeReserveGuardConfig_output setFeeReserveGuardConfig(const id& caller, uint64 dropBasisPoints, uint64 windowSeconds) + { + NOST::SetFeeReserveGuardConfig_input input{}; + NOST::SetFeeReserveGuardConfig_output output{}; + + input.dropBasisPoints = dropBasisPoints; + input.windowSeconds = windowSeconds; + ensureUser(caller); + invokeUserProcedure(NOST_CONTRACT_INDEX, 9, input, output, caller, 0); + return output; + } + + NOST::SetEmergencyPause_output setEmergencyPause(const id& caller, bool paused) + { + NOST::SetEmergencyPause_input input{}; + NOST::SetEmergencyPause_output output{}; + + input.paused = paused ? 1 : 0; + ensureUser(caller); + invokeUserProcedure(NOST_CONTRACT_INDEX, 10, input, output, caller, 0); + return output; + } + + NOST::StateData& stateData() { return *reinterpret_cast(contractStates[NOST_CONTRACT_INDEX]); } + const NOST::StateData& stateData() const { return *reinterpret_cast(contractStates[NOST_CONTRACT_INDEX]); } + QX::StateData& qxStateData() { return *reinterpret_cast(contractStates[QX_CONTRACT_INDEX]); } + + void setRouteAllFeesToDevelopment(uint8 enabled) { stateData().routeAllFeesToDevelopment = enabled; } + uint8 getRouteAllFeesToDevelopment() const { return stateData().routeAllFeesToDevelopment; } + void syncCachedQxTransferFee() { stateData().qxTransferFee = qxStateData()._transferFee; } + uint32 getCachedQxTransferFee() const { return stateData().qxTransferFee; } + + sint64 managedShares(const Asset& asset, const id& owner) const + { + return numberOfPossessedShares(asset.assetName, asset.issuer, owner, owner, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX); + } + + sint64 sharesManagedBy(const Asset& asset, const id& owner, uint32 contractIndex) const + { + return numberOfPossessedShares(asset.assetName, asset.issuer, owner, owner, contractIndex, contractIndex); + } + + sint64 plainShares(const Asset& asset, const id& owner) const + { + return numberOfShares(asset, AssetOwnershipSelect::byOwner(owner), AssetPossessionSelect::byPossessor(owner)); + } + + static Array makeMetadataCid() + { + Array cid{}; + const char* cidText = "bafybeigdyrzt2a3x4m5n6p7qrstuvwx234567abcdefghijklmnopqrst"; + for (uint64 i = 0; cidText[i] != 0 && i < NOST_AUCTION_METADATA_CID_LENGTH; ++i) + { + cid.set(i, static_cast(cidText[i])); + } + return cid; + } + + static Array makeInvalidMetadataCidFirstChar() + { + auto cid = makeMetadataCid(); + cid.set(0, 'c'); + return cid; + } + + static Array makeInvalidMetadataCidUppercase() + { + auto cid = makeMetadataCid(); + cid.set(5, 'A'); + return cid; + } + + static Array makeSingleLot(const Asset& asset, sint64 quantity) + { + Array lot{}; + NOST::AuctionAssetEntry entry{}; + + entry.asset = asset; + entry.quantity = quantity; + lot.set(0, entry); + return lot; + } + + static Array makeLot(std::initializer_list entries) + { + Array lot{}; + uint64 index = 0; + for (const auto& entry : entries) + { + lot.set(index++, entry); + } + return lot; + } + + static Array makeAllowedWallets(std::initializer_list wallets) + { + Array allowed{}; + uint64 index = 0; + for (const auto& wallet : wallets) + { + allowed.set(index++, wallet); + } + return allowed; + } + + static Array + makeRequiredAccessAssets(std::initializer_list assets) + { + Array required{}; + uint64 index = 0; + for (const auto& asset : assets) + { + required.set(index++, asset); + } + return required; + } + + static NOST::CreateAuction_input makeBatchAuctionInput(const Asset& asset, sint64 quantity, uint64 salePrice = 10) + { + NOST::CreateAuction_input input{}; + input.metadataIpfsCid = makeMetadataCid(); + input.auctionLotItems = makeSingleLot(asset, quantity); + input.minimumPurchaseQuantity = 1; + input.salePrice = salePrice; + input.durationDays = 1; + input.auctionType = static_cast(NOST::EAuctionType::Batch); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Public); + return input; + } + + static NOST::CreateAuction_input makeStandardAuctionInput(const Array& lot, + uint64 initialPrice = NOST_STANDARD_MIN_PRICE, + uint64 salePrice = NOST_STANDARD_MIN_PRICE, + uint64 minimumBidIncrement = NOST_STANDARD_MIN_BID_INCREMENT, uint64 buyNowPrice = 0) + { + NOST::CreateAuction_input input{}; + input.metadataIpfsCid = makeMetadataCid(); + input.auctionLotItems = lot; + input.minimumPurchaseQuantity = 1; + input.initialPrice = initialPrice; + input.salePrice = salePrice; + input.minimumBidIncrement = minimumBidIncrement; + input.buyNowPrice = buyNowPrice; + input.durationDays = 1; + input.auctionType = static_cast(NOST::EAuctionType::Standard); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Public); + return input; + } + + sint64 expectedDividendPoolIncrease(uint64 addedDividendAmount) const + { + const uint64 poolBefore = stateData().auctionShareholderDividendPool; + const uint64 poolAfterFunding = poolBefore + addedDividendAmount; + return static_cast(poolAfterFunding % NUMBER_OF_COMPUTORS) - static_cast(poolBefore); + } + + static id managementWallet() + { + return ID(_I, _G, _P, _Z, _X, _Q, _O, _R, _J, _Y, _Q, _P, _A, _G, _V, _A, _B, _N, _T, _N, _I, _S, _O, _Y, _T, _M, _T, _A, _N, _M, _K, _Z, _A, + _S, _T, _P, _P, _G, _Z, _O, _N, _A, _Q, _J, _X, _Q, _O, _S, _W, _Q, _O, _V, _J, _C, _K, _D); + } + + static id developmentWallet() + { + return ID(_D, _Q, _V, _H, _M, _Z, _F, _C, _W, _O, _K, _M, _H, _F, _B, _H, _L, _X, _U, _I, _U, _G, _P, _P, _X, _R, _Z, _C, _U, _V, _S, _N, _J, + _F, _Z, _J, _F, _M, _Q, _M, _Y, _D, _B, _X, _E, _S, _E, _A, _T, _M, _W, _L, _K, _N, _L, _D); + } + + static id takeoverCoordinatorWallet() + { + return ID(_X, _J, _O, _S, _N, _L, _T, _Z, _V, _V, _H, _N, _Z, _C, _B, _Y, _X, _I, _E, _V, _N, _E, _P, _P, _B, _O, _Q, _A, _W, _D, _B, _V, _G, + _E, _N, _Z, _O, _X, _S, _V, _O, _B, _K, _G, _Z, _C, _C, _F, _D, _B, _D, _M, _T, _M, _L, _C); + } +}; -static unsigned long long random(unsigned long long minValue, unsigned long long maxValue) +static bool containsWallet(const Array& wallets, uint64 count, const id& wallet) { - if(minValue > maxValue) - { - return 0; - } - return minValue + rand64() % (maxValue - minValue); + for (uint64 index = 0; index < count; ++index) + { + if (wallets.get(index) == wallet) + { + return true; + } + } + return false; } -static id getUser(unsigned long long i) +static bool containsAccessAsset(const Array& assets, uint64 count, + const NOST::AuctionAssetEntry& expected) { - return id(i, i / 2 + 4, i + 10, i * 3 + 8); + for (uint64 index = 0; index < count; ++index) + { + if (assets.get(index).asset == expected.asset && assets.get(index).quantity == expected.quantity) + { + return true; + } + } + return false; } -static std::vector getRandomUsers(unsigned int totalUsers, unsigned int maxNum) +static bool containsAuctionIndex(const Array& auctionIndices, uint64 count, uint64 auctionIndex) { - unsigned long long userCount = random(0, maxNum); - std::vector users; - users.reserve(userCount); - for (unsigned int i = 0; i < userCount; ++i) - { - unsigned long long userIdx = random(0, totalUsers - 1); - users.push_back(getUser(userIdx)); - } - return users; + const uint64 boundedCount = count < auctionIndices.capacity() ? count : auctionIndices.capacity(); + for (uint64 index = 0; index < boundedCount; ++index) + { + if (auctionIndices.get(index) == auctionIndex) + { + return true; + } + } + return false; } -class NostromoChecker : public NOST, public NOST::StateData +static void expectAuctionFeesEqual(const NOST::GetAuctionFees_output& actual, const NOST::GetAuctionFees_output& expected) { -public: - void registerChecker(id registerId, uint32 tierLevel, uint32 indexOfRegister) - { - EXPECT_EQ(users.contains(registerId), 1); - uint8 stateTierLevel; - users.get(registerId, stateTierLevel); - EXPECT_EQ(tierLevel, stateTierLevel); - } - void countOfRegisterChecker(uint32 totalUser) - { - EXPECT_EQ(totalUser, numberOfRegister); - } - void logoutFromTierChecker(id registerId) - { - EXPECT_EQ(users.contains(registerId), 0); - } - void numberOfCreatedProjectChecker(uint32 numberOfProjects) - { - EXPECT_EQ(numberOfProjects, numberOfCreatedProject); - } - void createdProjectChecker(uint32 indexOfProject, id creator, uint64 assetName, uint32 supply, uint32 startYear, uint32 startMonth, uint32 startDay, uint32 startHour, uint32 endYear, uint32 endMonth, uint32 endDay, uint32 endHour) - { - uint32 startDate, endDate; - NOST::packNostromoDate(startYear, startMonth, startDay, startHour, 0, 0, startDate); - NOST::packNostromoDate(endYear, endMonth, endDay, endHour, 0, 0, endDate); - - EXPECT_EQ(tokens.contains(assetName), 1); - EXPECT_EQ(projects.get(indexOfProject).creator, creator); - EXPECT_EQ(projects.get(indexOfProject).isCreatedFundarasing, 0); - EXPECT_EQ(projects.get(indexOfProject).numberOfNo, 0); - EXPECT_EQ(projects.get(indexOfProject).numberOfYes, 0); - EXPECT_EQ(projects.get(indexOfProject).supplyOfToken, supply); - EXPECT_EQ(projects.get(indexOfProject).tokenName, assetName); - EXPECT_EQ(projects.get(indexOfProject).startDate, startDate); - EXPECT_EQ(projects.get(indexOfProject).endDate, endDate); - } - void epochRevenueChecker(uint64 amountOfRevenue) - { - EXPECT_EQ(amountOfRevenue, epochRevenue); - } - void totalPoolWeightChecker(uint32 totalWeight) - { - EXPECT_EQ(totalWeight, totalPoolWeight); - } - void voteInProjectChecker(uint32 indexOfProject, uint32 numberOfYes, uint32 numberOfNo) - { - EXPECT_EQ(projects.get(indexOfProject).numberOfYes, numberOfYes); - EXPECT_EQ(projects.get(indexOfProject).numberOfNo, numberOfNo); - } - void numberOfVotedProjectAndVotedListChecker(id registerId, uint32 numberOfProject, Array votedList) - { - uint32 count; - numberOfVotedProject.get(registerId, count); - EXPECT_EQ(count, numberOfProject); - - Array vote; - voteStatus.get(registerId, vote); - for (uint32 i = 0; i < count; i++) - { - EXPECT_EQ(vote.get(i), votedList.get(i)); - } - } - void countOfFundraisingChecker(uint32 count) - { - EXPECT_EQ(count, numberOfFundraising); - } - void createFundraisingChecker(const id& registerId, - uint64 tokenPrice, - uint64 soldAmount, - uint64 requiredFunds, - - uint32 indexOfProject, - uint32 firstPhaseStartYear, - uint32 firstPhaseStartMonth, - uint32 firstPhaseStartDay, - uint32 firstPhaseStartHour, - uint32 firstPhaseEndYear, - uint32 firstPhaseEndMonth, - uint32 firstPhaseEndDay, - uint32 firstPhaseEndHour, - - uint32 secondPhaseStartYear, - uint32 secondPhaseStartMonth, - uint32 secondPhaseStartDay, - uint32 secondPhaseStartHour, - uint32 secondPhaseEndYear, - uint32 secondPhaseEndMonth, - uint32 secondPhaseEndDay, - uint32 secondPhaseEndHour, - - uint32 thirdPhaseStartYear, - uint32 thirdPhaseStartMonth, - uint32 thirdPhaseStartDay, - uint32 thirdPhaseStartHour, - uint32 thirdPhaseEndYear, - uint32 thirdPhaseEndMonth, - uint32 thirdPhaseEndDay, - uint32 thirdPhaseEndHour, - - uint32 listingStartYear, - uint32 listingStartMonth, - uint32 listingStartDay, - uint32 listingStartHour, - - uint32 cliffEndYear, - uint32 cliffEndMonth, - uint32 cliffEndDay, - uint32 cliffEndHour, - - uint32 vestingEndYear, - uint32 vestingEndMonth, - uint32 vestingEndDay, - uint32 vestingEndHour, - - uint8 threshold, - uint8 TGE, - uint8 stepOfVesting, - - uint32 indexOfFundraising) - { - uint32 firstPhaseStartDate_t, secondPhaseStartDate_t, thirdPhaseStartDate_t, firstPhaseEndDate_t, secondPhaseEndDate_t, thirdPhaseEndDate_t, listingStartDate_t, cliffEndDate_t, vestingEndDate_t; - NOST::packNostromoDate(firstPhaseStartYear, firstPhaseStartMonth, firstPhaseStartDay, firstPhaseStartHour, 0, 0, firstPhaseStartDate_t); - NOST::packNostromoDate(secondPhaseStartYear, secondPhaseStartMonth, secondPhaseStartDay, secondPhaseStartHour, 0, 0, secondPhaseStartDate_t); - NOST::packNostromoDate(thirdPhaseStartYear, thirdPhaseStartMonth, thirdPhaseStartDay, thirdPhaseStartHour, 0, 0, thirdPhaseStartDate_t); - NOST::packNostromoDate(firstPhaseEndYear, firstPhaseEndMonth, firstPhaseEndDay, firstPhaseEndHour, 0, 0, firstPhaseEndDate_t); - NOST::packNostromoDate(secondPhaseEndYear, secondPhaseEndMonth, secondPhaseEndDay, secondPhaseEndHour, 0, 0, secondPhaseEndDate_t); - NOST::packNostromoDate(thirdPhaseEndYear, thirdPhaseEndMonth, thirdPhaseEndDay, thirdPhaseEndHour, 0, 0, thirdPhaseEndDate_t); - NOST::packNostromoDate(listingStartYear, listingStartMonth, listingStartDay, listingStartHour, 0, 0, listingStartDate_t); - NOST::packNostromoDate(cliffEndYear, cliffEndMonth, cliffEndDay, cliffEndHour, 0, 0, cliffEndDate_t); - NOST::packNostromoDate(vestingEndYear, vestingEndMonth, vestingEndDay, vestingEndHour, 0, 0, vestingEndDate_t); - - EXPECT_EQ(registerId, projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator); - EXPECT_EQ(tokenPrice, fundaraisings.get(indexOfFundraising).tokenPrice); - - EXPECT_EQ(soldAmount, fundaraisings.get(indexOfFundraising).soldAmount); - EXPECT_EQ(requiredFunds, fundaraisings.get(indexOfFundraising).requiredFunds); - EXPECT_EQ(indexOfProject, fundaraisings.get(indexOfFundraising).indexOfProject); - EXPECT_EQ(firstPhaseStartDate_t, fundaraisings.get(indexOfFundraising).firstPhaseStartDate); - EXPECT_EQ(secondPhaseStartDate_t, fundaraisings.get(indexOfFundraising).secondPhaseStartDate); - EXPECT_EQ(thirdPhaseStartDate_t, fundaraisings.get(indexOfFundraising).thirdPhaseStartDate); - EXPECT_EQ(firstPhaseEndDate_t, fundaraisings.get(indexOfFundraising).firstPhaseEndDate); - EXPECT_EQ(secondPhaseEndDate_t, fundaraisings.get(indexOfFundraising).secondPhaseEndDate); - EXPECT_EQ(thirdPhaseEndDate_t, fundaraisings.get(indexOfFundraising).thirdPhaseEndDate); - EXPECT_EQ(listingStartDate_t, fundaraisings.get(indexOfFundraising).listingStartDate); - EXPECT_EQ(cliffEndDate_t, fundaraisings.get(indexOfFundraising).cliffEndDate); - EXPECT_EQ(vestingEndDate_t, fundaraisings.get(indexOfFundraising).vestingEndDate); - EXPECT_EQ(threshold, fundaraisings.get(indexOfFundraising).threshold); - EXPECT_EQ(TGE, fundaraisings.get(indexOfFundraising).TGE); - EXPECT_EQ(stepOfVesting, fundaraisings.get(indexOfFundraising).stepOfVesting); - - } - uint8 getTierLevel(id registerId) - { - if (users.contains(registerId)) - { - uint8 tierLevel; - users.get(registerId, tierLevel); - return tierLevel; - } - return 0; - } - uint64 getInvestedAmount(uint32 indexOfFundraising, id registerId) - { - investors.get(registerId, tmpInvestedList); - uint32 numberOfProject; - numberOfInvestedProjects.get(registerId, numberOfProject); - - for (uint32 i = 0; i < numberOfProject; i++) + EXPECT_EQ(actual.privateAuctionFee, expected.privateAuctionFee); + EXPECT_EQ(actual.publicAuctionCreationFee, expected.publicAuctionCreationFee); + EXPECT_EQ(actual.auctionCancellationFeeBasisPoints, expected.auctionCancellationFeeBasisPoints); + EXPECT_EQ(actual.managementFeeBasisPoints, expected.managementFeeBasisPoints); + EXPECT_EQ(actual.developmentFeeBasisPoints, expected.developmentFeeBasisPoints); + EXPECT_EQ(actual.takeoverCoordinatorFeeBasisPoints, expected.takeoverCoordinatorFeeBasisPoints); + EXPECT_EQ(actual.shareholderDividendBasisPoints, expected.shareholderDividendBasisPoints); + EXPECT_EQ(actual.shareholderFeeBasisPointsTier1, expected.shareholderFeeBasisPointsTier1); + EXPECT_EQ(actual.shareholderFeeBasisPointsTier2, expected.shareholderFeeBasisPointsTier2); + EXPECT_EQ(actual.shareholderFeeBasisPointsTier3, expected.shareholderFeeBasisPointsTier3); + EXPECT_EQ(actual.shareholderFeeBasisPointsTier4, expected.shareholderFeeBasisPointsTier4); +} + +TEST(ContractNostromoAuction, InitialStateAndGettersAuction) +{ + ContractTestingNOST nostromo; + + const auto fees = nostromo.getAuctionFees(); + EXPECT_EQ(fees.privateAuctionFee, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + EXPECT_EQ(fees.publicAuctionCreationFee, NOST_PUBLIC_AUCTION_CREATION_FEE); + EXPECT_EQ(fees.auctionCancellationFeeBasisPoints, NOST_DEFAULT_AUCTION_CANCELLATION_FEE_BP); + EXPECT_EQ(fees.managementFeeBasisPoints, NOST_DEFAULT_AUCTION_MANAGEMENT_FEE_BP); + EXPECT_EQ(fees.developmentFeeBasisPoints, NOST_DEFAULT_AUCTION_DEVELOPMENT_FEE_BP); + EXPECT_EQ(fees.takeoverCoordinatorFeeBasisPoints, NOST_DEFAULT_AUCTION_TAKEOVER_COORDINATOR_FEE_BP); + EXPECT_EQ(fees.shareholderDividendBasisPoints, NOST_DEFAULT_AUCTION_SHAREHOLDER_DIVIDEND_BP); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier1, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_1); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier2, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_2); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier3, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_3); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier4, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4); + + const auto recipients = nostromo.getFeeRecipients(); + EXPECT_EQ(recipients.management, ContractTestingNOST::managementWallet()); + EXPECT_EQ(recipients.development, ContractTestingNOST::developmentWallet()); + EXPECT_EQ(recipients.takeoverCoordinator, ContractTestingNOST::takeoverCoordinatorWallet()); + + const uint64 missingAuction = 777; + const id missingParticipant(888, 0, 0, 0); + const auto auctionOutput = nostromo.getAuction(missingAuction); + const auto participantOutput = nostromo.getParticipant(missingAuction, missingParticipant); + const auto launchPause = nostromo.getTicksBeforeAuctionLaunch(); + + EXPECT_EQ(auctionOutput.auction.core.auctionIndex, 0ULL); + EXPECT_EQ(participantOutput.found, 0); + EXPECT_EQ(launchPause.ticks, 0U); + EXPECT_EQ(nostromo.getClosedAuctionHistory().totalEntries, 0ULL); + EXPECT_EQ(nostromo.getRouteAllFeesToDevelopmentPublic().enabled, NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT); + + nostromo.setRouteAllFeesToDevelopment(1); + EXPECT_EQ(nostromo.getRouteAllFeesToDevelopmentPublic().enabled, 1); + nostromo.setRouteAllFeesToDevelopment(0); + EXPECT_EQ(nostromo.getRouteAllFeesToDevelopmentPublic().enabled, 0); + + nostromo.beginEpoch(); + EXPECT_EQ(nostromo.getCachedQxTransferFee(), nostromo.qxStateData()._transferFee); + EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, 0U); +} + +TEST(ContractNostromoAuction, AuctionIndexAndExpandedGetterSurfaceAuction) +{ + ContractTestingNOST nostromo; + const id sellerA(31, 32, 33, 34); + const id sellerB(35, 36, 37, 38); + const id bidderA(39, 40, 41, 42); + const id bidderB(43, 44, 45, 46); + const uint64 assetNameA = assetNameFromString("IDXGTA"); + const uint64 assetNameB = assetNameFromString("IDXGTB"); + const uint64 assetNameC = assetNameFromString("IDXGTC"); + const Asset assetA{sellerA, assetNameA}; + const Asset assetB{sellerA, assetNameB}; + const Asset assetC{sellerB, assetNameC}; + + EXPECT_EQ(nostromo.getLatestAuctionIndex().found, 0); + EXPECT_EQ(nostromo.getLatestAuctionIndex().auctionIndex, 0ULL); + + EXPECT_EQ(nostromo.issueAsset(sellerA, assetNameA, 3), 3); + EXPECT_EQ(nostromo.issueAsset(sellerA, assetNameB, 2), 2); + EXPECT_EQ(nostromo.issueAsset(sellerB, assetNameC, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(sellerA, assetA, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(sellerA, assetB, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(sellerB, assetC, 1), 1); + + auto inputA = ContractTestingNOST::makeBatchAuctionInput(assetA, 3, 10); + auto inputB = ContractTestingNOST::makeBatchAuctionInput(assetB, 2, 12); + auto inputC = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetC, 1)); + inputB.metadataIpfsCid.set(10, '2'); + inputC.metadataIpfsCid.set(10, '3'); + + const auto createA = nostromo.createAuction(sellerA, inputA); + const auto createB = nostromo.createAuction(sellerA, inputB); + const auto createC = nostromo.createAuction(sellerB, inputC); + ASSERT_EQ(createA.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(createB.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(createC.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(createA.auctionIndex, 0ULL); + EXPECT_EQ(createB.auctionIndex, 1ULL); + EXPECT_EQ(createC.auctionIndex, 2ULL); + + EXPECT_EQ(nostromo.getLatestAuctionIndex().found, 1); + EXPECT_EQ(nostromo.getLatestAuctionIndex().auctionIndex, 2ULL); + EXPECT_EQ(nostromo.getAuction(createB.auctionIndex).auction.core.auctionIndex, 1ULL); + EXPECT_EQ(nostromo.getAuctionAtCreationSnapshot(createC.auctionIndex).seller, sellerB); + EXPECT_EQ(nostromo.getAuctionAtCreationSnapshot(createC.auctionIndex).auctionIndex, 2ULL); + + const auto summaries = nostromo.getAuctionSummaries(0, 64); + EXPECT_EQ(summaries.totalCount, 3ULL); + EXPECT_EQ(summaries.returnedCount, 3ULL); + EXPECT_EQ(summaries.auctions.get(0).auctionIndex, 0ULL); + EXPECT_EQ(summaries.auctions.get(2).seller, sellerB); + + const auto sellerAList = nostromo.getAuctionsBySeller(sellerA, 0, 64); + EXPECT_EQ(sellerAList.totalCount, 2ULL); + EXPECT_EQ(sellerAList.returnedCount, 2ULL); + EXPECT_EQ(nostromo.getAuctionCountBySeller(sellerA).count, 2ULL); + EXPECT_EQ(nostromo.getAuctionCountBySeller(sellerB).count, 1ULL); + + const auto metadataLookup = nostromo.getAuctionByMetadataCid(inputB.metadataIpfsCid); + EXPECT_EQ(metadataLookup.found, 1); + EXPECT_EQ(metadataLookup.auctionIndex, 1ULL); + EXPECT_EQ(metadataLookup.auction.seller, sellerA); + + Array requestedIndices{}; + requestedIndices.set(0, createC.auctionIndex); + requestedIndices.set(1, 999); + requestedIndices.set(2, createA.auctionIndex); + const auto batch = nostromo.getAuctionSummariesByIndexBatch(requestedIndices, 3); + EXPECT_EQ(batch.returnedCount, 2ULL); + EXPECT_EQ(batch.found.get(0), 1); + EXPECT_EQ(batch.found.get(1), 0); + EXPECT_EQ(batch.found.get(2), 1); + EXPECT_EQ(batch.auctions.get(0).auctionIndex, 2ULL); + EXPECT_EQ(batch.auctions.get(2).auctionIndex, 0ULL); + + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createA.auctionIndex, 2, 11).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderB, createA.auctionIndex, 1, 15).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidderA, createC.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, + NOST::EAuctionError::Success); + + const auto active = nostromo.getActiveAuctionIndices(0, 64); + EXPECT_EQ(active.totalCount, 3ULL); + EXPECT_EQ(active.returnedCount, 3ULL); + EXPECT_EQ(active.auctionIndices.get(1), 1ULL); + + const auto participants = nostromo.getAuctionParticipants(createA.auctionIndex, 0, 64); + EXPECT_EQ(participants.totalCount, 2ULL); + EXPECT_EQ(participants.returnedCount, 2ULL); + EXPECT_TRUE(participants.participants.get(0).participant == bidderA || participants.participants.get(1).participant == bidderA); + + const auto bidderAParticipations = nostromo.getUserParticipations(bidderA, 0, 64); + EXPECT_EQ(bidderAParticipations.totalCount, 2ULL); + EXPECT_EQ(bidderAParticipations.returnedCount, 2ULL); + + const auto stats = nostromo.getContractStats(); + EXPECT_EQ(stats.stats.totalAuctionsCreated, 3ULL); + EXPECT_EQ(stats.stats.activeAuctionCount, 3ULL); + EXPECT_EQ(stats.stats.participantCount, 3ULL); +} + +TEST(ContractNostromoAuction, RetainedAuctionGetterPaginationAcrossLiveAndClosedStorageAuction) +{ + ContractTestingNOST nostromo; + const id sellerA(61, 62, 63, 64); + const id sellerB(65, 66, 67, 68); + const id missingSeller(69, 70, 71, 72); + const auto makeAuction = + [](uint64 auctionIndex, const id& seller, NOST::EAuctionStatus status, const Array& metadataCid) + { + NOST::AuctionData auction{}; + auction.core.auctionIndex = auctionIndex; + auction.core.seller = seller; + auction.core.status = status; + auction.core.metadataIpfsCid = metadataCid; + return auction; + }; + auto sharedCid = ContractTestingNOST::makeMetadataCid(); + auto cidAtIndex1 = ContractTestingNOST::makeMetadataCid(); + auto cidAtIndex5 = ContractTestingNOST::makeMetadataCid(); + auto cidAtIndex6 = ContractTestingNOST::makeMetadataCid(); + auto cidAtIndex7 = ContractTestingNOST::makeMetadataCid(); + auto missingCid = ContractTestingNOST::makeMetadataCid(); + sharedCid.set(10, 's'); + cidAtIndex1.set(10, 'a'); + cidAtIndex5.set(10, 'c'); + cidAtIndex6.set(10, 'd'); + cidAtIndex7.set(10, 'e'); + missingCid.set(10, 'm'); + + // Insert live auctions out of creation order to ensure pagination does not depend on physical hash-map order. + ASSERT_NE(nostromo.stateData().auctionList.set(7, makeAuction(7, sellerA, NOST::EAuctionStatus::Active, cidAtIndex7)), NULL_INDEX); + ASSERT_NE(nostromo.stateData().auctionList.set(1, makeAuction(1, sellerB, NOST::EAuctionStatus::Active, cidAtIndex1)), NULL_INDEX); + ASSERT_NE(nostromo.stateData().auctionList.set(9, makeAuction(9, sellerA, NOST::EAuctionStatus::Active, sharedCid)), NULL_INDEX); + ASSERT_NE(nostromo.stateData().auctionList.set(5, makeAuction(5, sellerA, NOST::EAuctionStatus::PendingSellerDecision, cidAtIndex5)), NULL_INDEX); + + // A partially filled history verifies that uninitialized ring capacity is not scanned as retained data. + nostromo.stateData().closedAuctionHistory.set(0, makeAuction(2, sellerA, NOST::EAuctionStatus::Finalized, sharedCid)); + nostromo.stateData().closedAuctionHistory.set(1, makeAuction(6, sellerB, NOST::EAuctionStatus::Cancelled, cidAtIndex6)); + nostromo.stateData().closedAuctionHistoryCounter = 2; + + const auto summaries = nostromo.getAuctionSummaries(1, 3); + ASSERT_EQ(summaries.totalCount, 6ULL); + ASSERT_EQ(summaries.returnedCount, 3ULL); + EXPECT_EQ(summaries.auctions.get(0).auctionIndex, 2ULL); + EXPECT_EQ(summaries.auctions.get(1).auctionIndex, 5ULL); + EXPECT_EQ(summaries.auctions.get(2).auctionIndex, 6ULL); + + const auto active = nostromo.getActiveAuctionIndices(1, 2); + ASSERT_EQ(active.totalCount, 4ULL); + ASSERT_EQ(active.returnedCount, 2ULL); + EXPECT_EQ(active.auctionIndices.get(0), 5ULL); + EXPECT_EQ(active.auctionIndices.get(1), 7ULL); + EXPECT_EQ(nostromo.getActiveAuctionIndices(0, 0).returnedCount, 0ULL); + EXPECT_EQ(nostromo.getActiveAuctionIndices(active.totalCount, 2).returnedCount, 0ULL); + + const auto sellerPage = nostromo.getAuctionsBySeller(sellerA, 1, 2); + ASSERT_EQ(sellerPage.totalCount, 4ULL); + ASSERT_EQ(sellerPage.returnedCount, 2ULL); + EXPECT_EQ(sellerPage.auctions.get(0).auctionIndex, 5ULL); + EXPECT_EQ(sellerPage.auctions.get(1).auctionIndex, 7ULL); + EXPECT_EQ(nostromo.getAuctionCountBySeller(sellerA).count, sellerPage.totalCount); + EXPECT_EQ(nostromo.getAuctionCountBySeller(sellerB).count, 2ULL); + EXPECT_EQ(nostromo.getAuctionCountBySeller(missingSeller).count, 0ULL); + EXPECT_EQ(nostromo.getAuctionsBySeller(missingSeller, 0, 2).returnedCount, 0ULL); + + const auto sharedCidLookup = nostromo.getAuctionByMetadataCid(sharedCid); + ASSERT_EQ(sharedCidLookup.found, 1); + EXPECT_EQ(sharedCidLookup.auctionIndex, 2ULL); + EXPECT_EQ(sharedCidLookup.auction.seller, sellerA); + EXPECT_EQ(nostromo.getAuctionByMetadataCid(missingCid).found, 0); +} + +TEST(ContractNostromoAuction, TransferShareManagementRightsAuction) +{ + ContractTestingNOST nostromo; + const id owner(1, 2, 3, 4); + const uint64 assetName = assetNameFromString("NOSTTR"); + const Asset asset{owner, assetName}; + + EXPECT_EQ(nostromo.issueAsset(owner, assetName, 10), 10); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(owner, asset, 7), 7); + EXPECT_EQ(nostromo.managedShares(asset, owner), 7); + EXPECT_EQ(nostromo.sharesManagedBy(asset, owner, QX_CONTRACT_INDEX), 3); + + const auto invalidZeroShares = nostromo.transferManagedShares(owner, asset, 0, QX_CONTRACT_INDEX); + EXPECT_EQ(invalidZeroShares.transferredNumberOfShares, 0); + EXPECT_EQ(invalidZeroShares.errorCode, NOST::EAuctionError::InvalidInput); + + Asset zeroAsset{}; + const auto invalidZeroAsset = nostromo.transferManagedShares(owner, zeroAsset, 1, QX_CONTRACT_INDEX); + EXPECT_EQ(invalidZeroAsset.transferredNumberOfShares, 0); + EXPECT_EQ(invalidZeroAsset.errorCode, NOST::EAuctionError::InvalidInput); + + const auto invalidZeroContract = nostromo.transferManagedShares(owner, asset, 1, 0); + EXPECT_EQ(invalidZeroContract.transferredNumberOfShares, 0); + EXPECT_EQ(invalidZeroContract.errorCode, NOST::EAuctionError::InvalidInput); + + const auto insufficient = nostromo.transferManagedShares(owner, asset, 8, QX_CONTRACT_INDEX); + EXPECT_EQ(insufficient.transferredNumberOfShares, 0); + EXPECT_EQ(insufficient.errorCode, NOST::EAuctionError::InvalidInput); + + const auto success = nostromo.transferManagedShares(owner, asset, 5, QX_CONTRACT_INDEX); + EXPECT_EQ(success.transferredNumberOfShares, 5); + EXPECT_EQ(success.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.managedShares(asset, owner), 2); + EXPECT_EQ(nostromo.sharesManagedBy(asset, owner, QX_CONTRACT_INDEX), 8); +} + +TEST(ContractNostromoAuction, TransferShareManagementRightsRequiresInvocationRewardAuction) +{ + { + ContractTestingNOST nostromo; + const id owner(5, 6, 7, 8); + const uint64 assetName = assetNameFromString("TRFEXA"); + const Asset asset{owner, assetName}; + + EXPECT_EQ(nostromo.issueAsset(owner, assetName, 4), 4); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(owner, asset, 4), 4); + nostromo.syncCachedQxTransferFee(); + + const auto output = nostromo.transferManagedSharesWithReward(owner, asset, 2, QX_CONTRACT_INDEX, nostromo.getCachedQxTransferFee()); + EXPECT_EQ(output.transferredNumberOfShares, 2); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.managedShares(asset, owner), 2); + EXPECT_EQ(nostromo.sharesManagedBy(asset, owner, QX_CONTRACT_INDEX), 2); + } + + { + ContractTestingNOST nostromo; + const id owner(9, 10, 11, 12); + const uint64 assetName = assetNameFromString("TRFINS"); + const Asset asset{owner, assetName}; + + EXPECT_EQ(nostromo.issueAsset(owner, assetName, 4), 4); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(owner, asset, 4), 4); + nostromo.syncCachedQxTransferFee(); + + const auto output = nostromo.transferManagedSharesWithReward(owner, asset, 2, QX_CONTRACT_INDEX, nostromo.getCachedQxTransferFee() - 1); + EXPECT_EQ(output.transferredNumberOfShares, 0); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.managedShares(asset, owner), 4); + EXPECT_EQ(nostromo.sharesManagedBy(asset, owner, QX_CONTRACT_INDEX), 0); + } + + { + ContractTestingNOST nostromo; + const id owner(13, 14, 15, 16); + const uint64 assetName = assetNameFromString("TRFEXC"); + const Asset asset{owner, assetName}; + + EXPECT_EQ(nostromo.issueAsset(owner, assetName, 4), 4); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(owner, asset, 4), 4); + nostromo.syncCachedQxTransferFee(); + const sint64 reward = static_cast(nostromo.getCachedQxTransferFee()) + 50; + nostromo.seedUser(owner, reward); + const sint64 ownerBefore = getBalance(owner); + + const auto output = nostromo.transferManagedSharesWithFundedReward(owner, asset, 2, QX_CONTRACT_INDEX, reward); + EXPECT_EQ(output.transferredNumberOfShares, 2); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(owner) - ownerBefore, -static_cast(nostromo.getCachedQxTransferFee())); + EXPECT_EQ(nostromo.managedShares(asset, owner), 2); + EXPECT_EQ(nostromo.sharesManagedBy(asset, owner, QX_CONTRACT_INDEX), 2); + } + + { + ContractTestingNOST nostromo; + const id owner(17, 18, 19, 20); + const uint64 assetName = assetNameFromString("TRFINV"); + const Asset asset{owner, assetName}; + + EXPECT_EQ(nostromo.issueAsset(owner, assetName, 4), 4); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(owner, asset, 4), 4); + nostromo.syncCachedQxTransferFee(); + + const auto invalidDestination = nostromo.transferManagedSharesWithReward(owner, asset, 2, 0, nostromo.getCachedQxTransferFee()); + EXPECT_EQ(invalidDestination.transferredNumberOfShares, 0); + EXPECT_EQ(invalidDestination.errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.managedShares(asset, owner), 4); + + Asset zeroAsset{}; + const auto zeroAssetOutput = + nostromo.transferManagedSharesWithReward(owner, zeroAsset, 2, QX_CONTRACT_INDEX, nostromo.getCachedQxTransferFee()); + EXPECT_EQ(zeroAssetOutput.transferredNumberOfShares, 0); + EXPECT_EQ(zeroAssetOutput.errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.managedShares(asset, owner), 4); + } +} +TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) +{ + ContractTestingNOST nostromo; + const id seller(11, 12, 13, 14); + const uint64 assetName = assetNameFromString("CRTBTN"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 9), 9); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 9), 9); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 9, 25); + const auto output = nostromo.createAuction(seller, input); + ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(output.auctionIndex, 0ULL); + + const auto auction = nostromo.getAuction(output.auctionIndex).auction; + EXPECT_EQ(auction.core.auctionIndex, output.auctionIndex); + EXPECT_EQ(auction.core.quantityForSale, 9ULL); + EXPECT_EQ(auction.core.minimumPurchaseQuantity, 1ULL); + EXPECT_EQ(auction.core.salePrice, 25ULL); + EXPECT_EQ(auction.core.auctionDurationSeconds, NOST_SECONDS_PER_DAY); + EXPECT_EQ(auction.core.seller, seller); + EXPECT_EQ(auction.core.type, NOST::EAuctionType::Batch); + EXPECT_EQ(auction.core.visibility, NOST::EAuctionVisibility::Public); + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Active); + EXPECT_EQ(auction.core.auctionLotItems.get(0).asset, asset); + EXPECT_EQ(auction.core.auctionLotItems.get(0).quantity, 9); + EXPECT_EQ(auction.core.metadataIpfsCid.get(0), 'b'); + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); + EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 9); +} + +TEST(ContractNostromoAuction, PublicAuctionCreationAccumulatesConfiguredFeeAndRefundsExcessAuction) +{ + ContractTestingNOST nostromo; + const id seller(901, 902, 903, 904); + const Asset asset{seller, assetNameFromString("BCRFEE")}; + constexpr sint64 configuredFee = 73; + const auto feeInput = nostromo.makeCoordinatorFeeInput(configuredFee); + ASSERT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), feeInput).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, configuredFee); + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); + const auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 1, 1); + nostromo.seedUser(seller, 1000); + const sint64 sellerBefore = getBalance(seller); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + uint64 expectedPool = nostromo.getPendingServiceFeePool().pendingServiceFeePool; + + const auto insufficient = nostromo.createAuctionWithFundedReward(seller, input, configuredFee - 1); + EXPECT_EQ(insufficient.errorCode, NOST::EAuctionError::InsufficientFunds); + EXPECT_EQ(getBalance(seller), sellerBefore); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + const auto exact = nostromo.createAuctionWithFundedReward(seller, input, configuredFee); + ASSERT_EQ(exact.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(seller), sellerBefore - configuredFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + configuredFee); + expectedPool += static_cast(configuredFee); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + constexpr sint64 excessReward = configuredFee + 37; + const auto excess = nostromo.createAuctionWithFundedReward(seller, input, excessReward); + ASSERT_EQ(excess.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(seller), sellerBefore - 2 * configuredFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + 2 * configuredFee); + expectedPool += static_cast(configuredFee); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + constexpr sint64 managementConfiguredFee = 29; + const auto managementFeeInput = nostromo.makeManagementFeeInput(managementConfiguredFee); + ASSERT_EQ(nostromo.setAuctionFeesByManagement(ContractTestingNOST::managementWallet(), managementFeeInput).errorCode, + NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, managementConfiguredFee); + const auto managementConfigured = nostromo.createAuctionWithFundedReward(seller, input, managementConfiguredFee); + ASSERT_EQ(managementConfigured.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(seller), sellerBefore - (2 * configuredFee + managementConfiguredFee)); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + 2 * configuredFee + managementConfiguredFee); + expectedPool += static_cast(managementConfiguredFee); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + const id standardSeller(921, 922, 923, 924); + const Asset standardAsset{standardSeller, assetNameFromString("BCFSTD")}; + ASSERT_EQ(nostromo.issueAsset(standardSeller, standardAsset.assetName, 3), 3); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(standardSeller, standardAsset, 3), 3); + const auto standardInput = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(standardAsset, 1)); + nostromo.seedUser(standardSeller, 1000); + const sint64 standardSellerBefore = getBalance(standardSeller); + const sint64 standardContractBefore = getBalance(NOST_CONTRACT_ID); + + const auto standardInsufficient = nostromo.createAuctionWithFundedReward(standardSeller, standardInput, managementConfiguredFee - 1); + EXPECT_EQ(standardInsufficient.errorCode, NOST::EAuctionError::InsufficientFunds); + EXPECT_EQ(getBalance(standardSeller), standardSellerBefore); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), standardContractBefore); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + const auto standardExact = nostromo.createAuctionWithFundedReward(standardSeller, standardInput, managementConfiguredFee); + ASSERT_EQ(standardExact.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(standardSeller), standardSellerBefore - managementConfiguredFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), standardContractBefore + managementConfiguredFee); + expectedPool += static_cast(managementConfiguredFee); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + constexpr sint64 standardExcessReward = managementConfiguredFee + 17; + const auto standardExcess = nostromo.createAuctionWithFundedReward(standardSeller, standardInput, standardExcessReward); + ASSERT_EQ(standardExcess.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(standardSeller), standardSellerBefore - 2 * managementConfiguredFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), standardContractBefore + 2 * managementConfiguredFee); + expectedPool += static_cast(managementConfiguredFee); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + const id privateSeller(925, 926, 927, 928); + const id allowedBidder(929, 930, 931, 932); + const Asset privateAsset{privateSeller, assetNameFromString("BCFPRV")}; + ASSERT_EQ(nostromo.issueAsset(privateSeller, privateAsset.assetName, 1), 1); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(privateSeller, privateAsset, 1), 1); + auto privateInput = ContractTestingNOST::makeBatchAuctionInput(privateAsset, 1, 1); + privateInput.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + privateInput.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); + nostromo.seedUser(privateSeller, NOST_DEFAULT_PRIVATE_AUCTION_FEE + 100); + const sint64 privateSellerBefore = getBalance(privateSeller); + EXPECT_EQ(nostromo.createAuctionWithFundedReward(privateSeller, privateInput, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, + NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(privateSeller), privateSellerBefore - NOST_DEFAULT_PRIVATE_AUCTION_FEE); + expectedPool += static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + const id privateStandardSeller(933, 934, 935, 936); + const Asset privateStandardAsset{privateStandardSeller, assetNameFromString("PRVSTD")}; + ASSERT_EQ(nostromo.issueAsset(privateStandardSeller, privateStandardAsset.assetName, 1), 1); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(privateStandardSeller, privateStandardAsset, 1), 1); + auto privateStandardInput = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(privateStandardAsset, 1)); + privateStandardInput.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + privateStandardInput.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); + nostromo.seedUser(privateStandardSeller, NOST_DEFAULT_PRIVATE_AUCTION_FEE + 100); + const sint64 privateStandardSellerBefore = getBalance(privateStandardSeller); + EXPECT_EQ(nostromo.createAuctionWithFundedReward(privateStandardSeller, privateStandardInput, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, + NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(privateStandardSeller), privateStandardSellerBefore - NOST_DEFAULT_PRIVATE_AUCTION_FEE); + expectedPool += static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, 0ULL); +} + +TEST(ContractNostromoAuction, BatchBidFeeBoundariesAuction) +{ + ContractTestingNOST nostromo; + const struct + { + uint64 bidQuantity; + uint64 bidAmount; + uint64 escrowAmount; + uint64 fee; + uint64 requiredReward; + } cases[] = { + {1, 9, 9, 91, 100}, {1, 10, 10, 90, 100}, {1, 20, 20, 80, 100}, {1, 30, 30, 70, 100}, {10, 100, 1000, 0, 1000}, + {1, 101, 101, 0, 101}, {2, 101, 202, 0, 202}, {2, 19, 38, 62, 100}, {2, 100, 200, 0, 200}, {UINT64_MAX, 2, UINT64_MAX, 0, UINT64_MAX}, + }; + + for (const auto& testCase : cases) + { + SCOPED_TRACE(::testing::Message() << "quantity=" << testCase.bidQuantity << ", bidAmount=" << testCase.bidAmount); + const auto output = nostromo.calculateBatchAuctionBidFee(testCase.bidQuantity, testCase.bidAmount); + EXPECT_EQ(output.escrowAmount, testCase.escrowAmount); + EXPECT_EQ(output.fee, testCase.fee); + EXPECT_EQ(output.requiredReward, testCase.requiredReward); + } +} + +TEST(ContractNostromoAuction, PublicAuctionCreationFeeConfigurationBoundariesAuction) +{ + ContractTestingNOST nostromo; + EXPECT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, NOST_PUBLIC_AUCTION_CREATION_FEE); + + auto coordinatorInput = nostromo.makeCoordinatorFeeInput(0); + ASSERT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, 0LL); + const id zeroFeeSeller(941, 942, 943, 944); + const Asset zeroFeeAsset{zeroFeeSeller, assetNameFromString("ZEROFEE")}; + ASSERT_EQ(nostromo.issueAsset(zeroFeeSeller, zeroFeeAsset.assetName, 1), 1); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(zeroFeeSeller, zeroFeeAsset, 1), 1); + EXPECT_EQ(nostromo.createAuctionWithFundedReward(zeroFeeSeller, ContractTestingNOST::makeBatchAuctionInput(zeroFeeAsset, 1, 1), 0).errorCode, + NOST::EAuctionError::Success); + + coordinatorInput.publicAuctionCreationFee = INT64_MAX; + ASSERT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, INT64_MAX); + + const auto feesBeforeInvalidUpdate = nostromo.getAuctionFees(); + coordinatorInput.publicAuctionCreationFee = -1; + coordinatorInput.auctionCancellationFeeBasisPoints = 0; + EXPECT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput).errorCode, + NOST::EAuctionError::InvalidInput); + const auto feesAfterInvalidUpdate = nostromo.getAuctionFees(); + EXPECT_EQ(feesAfterInvalidUpdate.publicAuctionCreationFee, feesBeforeInvalidUpdate.publicAuctionCreationFee); + EXPECT_EQ(feesAfterInvalidUpdate.auctionCancellationFeeBasisPoints, feesBeforeInvalidUpdate.auctionCancellationFeeBasisPoints); + + auto managementInput = nostromo.makeManagementFeeInput(41); + ASSERT_EQ(nostromo.setAuctionFeesByManagement(ContractTestingNOST::managementWallet(), managementInput).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, 41LL); +} + +TEST(ContractNostromoAuction, AcceptedBatchBidAccumulatesFeeAndKeepsEscrowAuction) +{ + ContractTestingNOST nostromo; + const id seller(905, 906, 907, 908); + const id firstBidder(909, 910, 911, 912); + const Asset asset{seller, assetNameFromString("BBDFEE")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 1)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + nostromo.seedUser(firstBidder, 1000); + const sint64 firstBidderBefore = getBalance(firstBidder); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + const uint64 poolBefore = nostromo.getPendingServiceFeePool().pendingServiceFeePool; + const auto underfundedSmallBid = nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 1, 9, 99); + EXPECT_EQ(underfundedSmallBid.errorCode, NOST::EAuctionError::InsufficientFunds); + EXPECT_EQ(underfundedSmallBid.refundedAmount, 99ULL); + EXPECT_EQ(getBalance(firstBidder), firstBidderBefore); + + const auto calculation = nostromo.calculateBatchAuctionBidFee(2, 40); + ASSERT_EQ(calculation.escrowAmount, 80ULL); + ASSERT_EQ(calculation.fee, 20ULL); + ASSERT_EQ(calculation.requiredReward, 100ULL); + const auto underfunded = nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 2, 40, calculation.requiredReward - 1); + EXPECT_EQ(underfunded.errorCode, NOST::EAuctionError::InsufficientFunds); + EXPECT_EQ(underfunded.refundedAmount, 99ULL); + EXPECT_EQ(getBalance(firstBidder), firstBidderBefore); + + const auto accepted = nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 2, 40, calculation.requiredReward + 49); + ASSERT_EQ(accepted.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(accepted.escrowedAmount, 80ULL); + EXPECT_EQ(accepted.refundedAmount, 49ULL); + EXPECT_EQ(getBalance(firstBidder), firstBidderBefore - 100); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + 100); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, poolBefore + 20ULL); + EXPECT_EQ(nostromo.getNostromoFeePool().feePool.developmentAmount, poolBefore + 20ULL); + + // The contract defaults to routing every fee to development, so the whole accumulated pool (including the earlier creation fee + // already reflected in contractBefore) leaves the contract at END_EPOCH, leaving only the escrowed amount behind. + ASSERT_EQ(nostromo.getRouteAllFeesToDevelopment(), NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT); + nostromo.endEpoch(); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, 0ULL); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + 80 - static_cast(poolBefore)); +} + +TEST(ContractNostromoAuction, CreateStandardSingleAssetAuctionEscrowsLotAuction) +{ + ContractTestingNOST nostromo; + const id seller(21, 22, 23, 24); + const uint64 assetName = assetNameFromString("CRTSTA"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 5), 5); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 5), 5); + + auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 5)); + input.minimumPurchaseQuantity = UINT64_MAX; + + nostromo.seedUser(seller, 1000); + const sint64 sellerBalanceBefore = getBalance(seller); + const sint64 contractBalanceBefore = getBalance(NOST_CONTRACT_ID); + const uint64 poolBefore = nostromo.getPendingServiceFeePool().pendingServiceFeePool; + const auto output = nostromo.createAuctionWithFundedReward(seller, input, NOST_PUBLIC_AUCTION_CREATION_FEE); + ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(seller), sellerBalanceBefore - NOST_PUBLIC_AUCTION_CREATION_FEE); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBalanceBefore + NOST_PUBLIC_AUCTION_CREATION_FEE); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, poolBefore + static_cast(NOST_PUBLIC_AUCTION_CREATION_FEE)); + + const auto auction = nostromo.getAuction(output.auctionIndex).auction; + EXPECT_EQ(auction.core.quantityForSale, 1ULL); + EXPECT_EQ(auction.core.minimumPurchaseQuantity, 0ULL); + EXPECT_EQ(auction.core.initialPrice, NOST_STANDARD_MIN_PRICE); + EXPECT_EQ(auction.core.salePrice, NOST_STANDARD_MIN_PRICE); + EXPECT_EQ(auction.core.minimumBidIncrement, NOST_STANDARD_MIN_BID_INCREMENT); + EXPECT_EQ(auction.core.type, NOST::EAuctionType::Standard); + EXPECT_EQ(auction.core.auctionLotItems.get(0).asset, asset); + EXPECT_EQ(auction.core.auctionLotItems.get(0).quantity, 5); + EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 5); +} + +TEST(ContractNostromoAuction, BatchMinimumPurchaseQuantityCreationBoundsAuction) +{ + ContractTestingNOST nostromo; + const id seller(301, 302, 303, 304); + const Asset asset{seller, assetNameFromString("BATMIN")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 30), 30); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 30), 30); + + auto zeroMinimum = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 10); + zeroMinimum.minimumPurchaseQuantity = 0; + EXPECT_EQ(nostromo.createAuction(seller, zeroMinimum).errorCode, NOST::EAuctionError::InvalidInput); + + auto excessiveMinimum = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 10); + excessiveMinimum.minimumPurchaseQuantity = 11; + EXPECT_EQ(nostromo.createAuction(seller, excessiveMinimum).errorCode, NOST::EAuctionError::InvalidInput); + + auto minimumOne = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 10); + const auto minimumOneOutput = nostromo.createAuction(seller, minimumOne); + ASSERT_EQ(minimumOneOutput.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getAuction(minimumOneOutput.auctionIndex).auction.core.minimumPurchaseQuantity, 1ULL); + + auto fullLotMinimum = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 10); + fullLotMinimum.minimumPurchaseQuantity = 10; + const auto fullLotMinimumOutput = nostromo.createAuction(seller, fullLotMinimum); + ASSERT_EQ(fullLotMinimumOutput.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getAuction(fullLotMinimumOutput.auctionIndex).auction.core.minimumPurchaseQuantity, 10ULL); +} + +TEST(ContractNostromoAuction, BatchBidEnforcesMinimumPurchaseQuantityAndRefundsAuction) +{ + ContractTestingNOST nostromo; + const id seller(305, 306, 307, 308); + const id bidder(309, 310, 311, 312); + const Asset asset{seller, assetNameFromString("BATBIDM")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 15), 15); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 15), 15); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 15, 10); + input.minimumPurchaseQuantity = 10; + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + nostromo.seedUser(bidder, 200); + const sint64 balanceBeforeRejectedBid = getBalance(bidder); + const auto rejectedBid = nostromo.placeBidWithFundedReward(bidder, createOutput.auctionIndex, 9, 10, 90); + EXPECT_EQ(rejectedBid.errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(rejectedBid.refundedAmount, 90ULL); + EXPECT_EQ(rejectedBid.escrowedAmount, 0ULL); + EXPECT_EQ(getBalance(bidder), balanceBeforeRejectedBid); + + const auto acceptedBid = nostromo.placeBatchBidWithFundedRequiredReward(bidder, createOutput.auctionIndex, 10, 10); + EXPECT_EQ(acceptedBid.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(acceptedBid.escrowedAmount, 100ULL); +} + +TEST(ContractNostromoAuction, CreateStandardAuctionSupportsFourLotEntriesAuction) +{ + ContractTestingNOST nostromo; + const id seller(25, 26, 27, 28); + const id bidder(29, 30, 31, 32); + const Asset assets[] = { + {seller, assetNameFromString("MAXLOA")}, + {seller, assetNameFromString("MAXLOB")}, + {seller, assetNameFromString("MAXLOC")}, + {seller, assetNameFromString("MAXLOD")}, + }; + + EXPECT_EQ(NOST_BATCH_AUCTION_LOT_ITEM_NUM, 1ULL); + EXPECT_EQ(NOST_AUCTION_LOT_ITEM_NUM, 4); + for (const auto& asset : assets) + { + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); + } + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeLot( + {{assets[0], 3}, {assets[1], 3}, {assets[2], 3}, {assets[3], 3}}))); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + for (const auto& asset : assets) + { + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); + EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 3); + } + + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, + NOST::EAuctionError::Success); + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + for (const auto& asset : assets) + { + EXPECT_EQ(nostromo.managedShares(asset, bidder), 3); + EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 0); + } +} + +TEST(ContractNostromoAuction, CreateBatchAuctionRejectsMultipleLotEntriesAuction) +{ + ContractTestingNOST nostromo; + const id seller(33, 34, 35, 36); + const Asset firstAsset{seller, assetNameFromString("BATLOA")}; + const Asset secondAsset{seller, assetNameFromString("BATLOB")}; + + EXPECT_EQ(nostromo.issueAsset(seller, firstAsset.assetName, 2), 2); + EXPECT_EQ(nostromo.issueAsset(seller, secondAsset.assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, firstAsset, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, secondAsset, 2), 2); + + auto input = ContractTestingNOST::makeBatchAuctionInput(firstAsset, 2); + input.auctionLotItems = ContractTestingNOST::makeLot({{firstAsset, 2}, {secondAsset, 2}}); + + EXPECT_EQ(nostromo.createAuction(seller, input).errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.managedShares(firstAsset, seller), 2); + EXPECT_EQ(nostromo.managedShares(secondAsset, seller), 2); +} + +TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(31, 32, 33, 34); + const id allowedBidder(35, 36, 37, 38); + const uint64 assetName = assetNameFromString("PRIWAL"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 4), 4); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 4), 4); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 4, 12); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); + + const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); + + const auto auction = nostromo.getAuction(output.auctionIndex).auction; + EXPECT_EQ(auction.core.visibility, NOST::EAuctionVisibility::Private); + EXPECT_EQ(auction.allowedBidderWalletCount, 1U); + EXPECT_EQ(auction.allowedBidderWallets.get(0), allowedBidder); + EXPECT_EQ(auction.requiredAccessAssetCount, 0U); + } + + { + ContractTestingNOST nostromo; + const id seller(41, 42, 43, 44); + const id gatedBidder(45, 46, 47, 48); + const uint64 saleAssetName = assetNameFromString("PRIACC"); + const uint64 gateAssetName = assetNameFromString("GATEAS"); + const Asset saleAsset{seller, saleAssetName}; + const Asset gateAsset{gatedBidder, gateAssetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, saleAssetName, 5), 5); + EXPECT_EQ(nostromo.issueAsset(gatedBidder, gateAssetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, saleAsset, 5), 5); + + auto input = ContractTestingNOST::makeBatchAuctionInput(saleAsset, 5, 20); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{gateAsset, 1}}); + + const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); + + const auto auction = nostromo.getAuction(output.auctionIndex).auction; + EXPECT_EQ(auction.core.visibility, NOST::EAuctionVisibility::Private); + EXPECT_EQ(auction.allowedBidderWalletCount, 0U); + EXPECT_EQ(auction.requiredAccessAssetCount, 1U); + EXPECT_EQ(auction.requiredAccessAssets.get(0).asset, gateAsset); + EXPECT_EQ(auction.requiredAccessAssets.get(0).quantity, 1); + EXPECT_GT(nostromo.plainShares(gateAsset, gatedBidder), 0); + } +} + +TEST(ContractNostromoAuction, GetAuctionViewExposesAccessListsAndFoundFlagAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(45, 46, 47, 48); + const id walletA(49, 50, 51, 52); + const id walletB(53, 54, 55, 56); + const uint64 assetName = assetNameFromString("VIEWWL"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({walletA, walletB}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto auctionOutput = nostromo.getAuction(createOutput.auctionIndex); + EXPECT_EQ(auctionOutput.found, 1); + EXPECT_EQ(auctionOutput.auction.core.auctionIndex, createOutput.auctionIndex); + EXPECT_EQ(auctionOutput.auction.allowedBidderWalletCount, 2U); + EXPECT_TRUE(containsWallet(auctionOutput.auction.allowedBidderWallets, auctionOutput.auction.allowedBidderWalletCount, walletA)); + EXPECT_TRUE(containsWallet(auctionOutput.auction.allowedBidderWallets, auctionOutput.auction.allowedBidderWalletCount, walletB)); + EXPECT_EQ(auctionOutput.auction.requiredAccessAssetCount, 0U); + } + + { + ContractTestingNOST nostromo; + const id seller(57, 58, 59, 60); + const id gateIssuerA(61, 62, 63, 64); + const id gateIssuerB(65, 66, 67, 68); + const uint64 assetName = assetNameFromString("VIEWAC"); + const Asset asset{seller, assetName}; + const Asset accessAssetA{gateIssuerA, assetNameFromString("GATEA1")}; + const Asset accessAssetB{gateIssuerB, assetNameFromString("GATEB1")}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.requiredAccessAssets = + ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAssetA, 2}, NOST::AuctionAssetEntry{accessAssetB, 5}}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto auctionOutput = nostromo.getAuction(createOutput.auctionIndex); + EXPECT_EQ(auctionOutput.found, 1); + EXPECT_EQ(auctionOutput.auction.requiredAccessAssetCount, 2U); + EXPECT_TRUE(containsAccessAsset(auctionOutput.auction.requiredAccessAssets, auctionOutput.auction.requiredAccessAssetCount, + NOST::AuctionAssetEntry{accessAssetA, 2})); + EXPECT_TRUE(containsAccessAsset(auctionOutput.auction.requiredAccessAssets, auctionOutput.auction.requiredAccessAssetCount, + NOST::AuctionAssetEntry{accessAssetB, 5})); + EXPECT_EQ(auctionOutput.auction.allowedBidderWalletCount, 0U); + } + + { + ContractTestingNOST nostromo; + const uint64 missingAuction = 999; + const auto auctionOutput = nostromo.getAuction(missingAuction); + EXPECT_EQ(auctionOutput.found, 0); + EXPECT_EQ(auctionOutput.auction.core.auctionIndex, 0ULL); + } +} + +TEST(ContractNostromoAuction, GetAuctionViewDeduplicatesPrivateAccessInputsAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(69, 70, 71, 72); + const id wallet(73, 74, 75, 76); + const uint64 assetName = assetNameFromString("DUPWAL"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({wallet, wallet}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.allowedBidderWalletCount, 1U); + EXPECT_TRUE(containsWallet(auction.allowedBidderWallets, auction.allowedBidderWalletCount, wallet)); + } + + { + ContractTestingNOST nostromo; + const id seller(77, 78, 79, 80); + const id gateIssuer(81, 82, 83, 84); + const uint64 assetName = assetNameFromString("DUPACC"); + const Asset asset{seller, assetName}; + const Asset accessAsset{gateIssuer, assetNameFromString("GATEDP")}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets( + {NOST::AuctionAssetEntry{accessAsset, 2}, NOST::AuctionAssetEntry{accessAsset, 5}, NOST::AuctionAssetEntry{accessAsset, 3}}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.requiredAccessAssetCount, 1U); + EXPECT_TRUE(containsAccessAsset(auction.requiredAccessAssets, auction.requiredAccessAssetCount, NOST::AuctionAssetEntry{accessAsset, 5})); + } +} + +TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacityAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(47, 48, 49, 50); + const id allowedBidder(30007, 31007, 32007, 33007); + const uint64 assetName = assetNameFromString("MAXWAL"); + const Asset asset{seller, assetName}; + Array allowedWallets{}; + EXPECT_EQ(NOST_AUCTION_ALLOWED_WALLET_NUM, 16ULL); + + for (uint64 index = 0; index < NOST_AUCTION_ALLOWED_WALLET_NUM; ++index) + { + allowedWallets.set(index, id(30000 + index, 31000 + index, 32000 + index, 33000 + index)); + } + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = allowedWallets; + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto auctionOutput = nostromo.getAuction(createOutput.auctionIndex); + EXPECT_EQ(auctionOutput.auction.allowedBidderWalletCount, NOST_AUCTION_ALLOWED_WALLET_NUM); + EXPECT_TRUE(containsWallet(auctionOutput.auction.allowedBidderWallets, auctionOutput.auction.allowedBidderWalletCount, allowedBidder)); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(allowedBidder, createOutput.auctionIndex, 1, 10).errorCode, NOST::EAuctionError::Success); + } + + { + ContractTestingNOST nostromo; + const id seller(57, 58, 59, 60); + const id accessBidder(61, 62, 63, 64); + const id gateIssuer(65, 66, 67, 68); + const uint64 saleAssetName = assetNameFromString("MAXACC"); + const uint64 bidderAccessAssetName = assetNameFromString("MAXACB"); + const Asset saleAsset{seller, saleAssetName}; + Array requiredAssets{}; + + for (uint64 index = 0; index < NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM; ++index) + { + requiredAssets.set(index, NOST::AuctionAssetEntry{Asset{gateIssuer, 34000 + index}, 1}); + } + requiredAssets.set(NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM - 1, NOST::AuctionAssetEntry{Asset{accessBidder, bidderAccessAssetName}, 1}); + + EXPECT_EQ(nostromo.issueAsset(seller, saleAssetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, saleAsset, 1), 1); + EXPECT_EQ(nostromo.issueAsset(accessBidder, bidderAccessAssetName, 1), 1); + + auto input = ContractTestingNOST::makeBatchAuctionInput(saleAsset, 1, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.requiredAccessAssets = requiredAssets; + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto auctionOutput = nostromo.getAuction(createOutput.auctionIndex); + EXPECT_EQ(auctionOutput.auction.requiredAccessAssetCount, NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM); + EXPECT_TRUE(containsAccessAsset(auctionOutput.auction.requiredAccessAssets, auctionOutput.auction.requiredAccessAssetCount, + NOST::AuctionAssetEntry{Asset{accessBidder, bidderAccessAssetName}, 1})); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(accessBidder, createOutput.auctionIndex, 1, 10).errorCode, NOST::EAuctionError::Success); + } +} + +TEST(ContractNostromoAuction, PrivateAuctionFeeIsDistributedAcrossRecipientsAuction) +{ + const uint8 routeModes[] = {0, 1}; + for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) + { + SCOPED_TRACE(::testing::Message() << "routeAllFeesToDevelopment=" << static_cast(routeModes[routeIndex])); + ContractTestingNOST nostromo; + const uint8 routeMode = routeModes[routeIndex]; + const id seller(61 + routeIndex, 62 + routeIndex, 63 + routeIndex, 64 + routeIndex); + const id allowedBidder(71 + routeIndex, 72 + routeIndex, 73 + routeIndex, 74 + routeIndex); + const uint64 assetName = assetNameFromString(routeMode ? "STDENR1" : "STDENR0"); + + const Asset asset{seller, assetName}; + + nostromo.setRouteAllFeesToDevelopment(routeMode); + EXPECT_EQ(nostromo.getRouteAllFeesToDevelopment(), routeMode); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 4), 4); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 4), 4); + + const sint64 sellerBefore = getBalance(seller); + const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + constexpr uint64 expectedShareholderDividend = 36350000ULL; + constexpr uint64 expectedManagementFee = 4550000ULL; + constexpr uint64 expectedDevelopmentFee = 4550000ULL; + constexpr uint64 expectedCoordinatorFee = 4550000ULL; + const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedShareholderDividend); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 4, 12); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); + + const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); + + EXPECT_EQ(getBalance(seller) - sellerBefore, 0); + + // The fee accumulates in the pool and is not distributed until END_EPOCH, regardless of the route-to-development mode. + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE)); + const auto pendingFeePool = nostromo.getNostromoFeePool(); + EXPECT_EQ(pendingFeePool.totalAmount, static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE)); + EXPECT_EQ(pendingFeePool.feePool.commonServiceFeeAmount, routeMode == 0 ? static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE) : 0ULL); + EXPECT_EQ(pendingFeePool.feePool.developmentAmount, routeMode != 0 ? static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE) : 0ULL); + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, 0ULL); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 0ULL); + + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); + } + else { - if (tmpInvestedList.get(i).indexOfFundraising == indexOfFundraising) + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedManagementFee); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedDevelopmentFee); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedCoordinatorFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); + } + } +} + +TEST(ContractNostromoAuction, EndEpochUsesCurrentManagementWalletForAccruedFeesAuction) +{ + ContractTestingNOST nostromo; + const id seller(1501, 1502, 1503, 1504); + const id allowedBidder(1505, 1506, 1507, 1508); + const id newManagement(1509, 1510, 1511, 1512); + const Asset asset{seller, assetNameFromString("CURMGR")}; + + nostromo.setRouteAllFeesToDevelopment(0); + ASSERT_EQ(nostromo.issueAsset(seller, asset.assetName, 1), 1); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); + ASSERT_EQ(nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, NOST::EAuctionError::Success); + + nostromo.ensureUser(newManagement); + const sint64 previousManagementBefore = getBalance(ContractTestingNOST::managementWallet()); + const sint64 newManagementBefore = getBalance(newManagement); + ASSERT_EQ(nostromo.setManagement(ContractTestingNOST::takeoverCoordinatorWallet(), newManagement).errorCode, NOST::EAuctionError::Success); + + nostromo.endEpoch(); + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()), previousManagementBefore); + EXPECT_EQ(getBalance(newManagement) - newManagementBefore, 4550000ULL); +} + +TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) +{ + ContractTestingNOST nostromo; + const id seller(51, 52, 53, 54); + const id altIssuer(55, 56, 57, 58); + const uint64 assetNameA = assetNameFromString("INVAAA"); + const Asset assetA{seller, assetNameA}; + const Asset accessAsset{altIssuer, assetNameFromString("GATINV")}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetNameA, 5), 5); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetA, 5), 5); + EXPECT_EQ(nostromo.issueAsset(altIssuer, assetNameFromString("GATINV"), 1), 1); + nostromo.seedUser(seller, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + const sint64 sellerBalanceBeforeInvalidCalls = getBalance(seller); + const sint64 contractBalanceBeforeInvalidCalls = getBalance(NOST_CONTRACT_ID); + const auto invokeRejectedPublicAuction = [&nostromo, &seller](const NOST::CreateAuction_input& input) { + return nostromo.createAuctionWithFundedReward(seller, input, NOST_PUBLIC_AUCTION_CREATION_FEE); + }; + const auto invokeRejectedPrivateAuction = [&nostromo, &seller](const NOST::CreateAuction_input& input) { + return nostromo.createAuctionWithFundedReward(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + }; + + auto invalidCid = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + invalidCid.metadataIpfsCid = ContractTestingNOST::makeInvalidMetadataCidFirstChar(); + EXPECT_EQ(invokeRejectedPublicAuction(invalidCid).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidCidUppercase = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + invalidCidUppercase.metadataIpfsCid = ContractTestingNOST::makeInvalidMetadataCidUppercase(); + EXPECT_EQ(invokeRejectedPublicAuction(invalidCidUppercase).errorCode, NOST::EAuctionError::InvalidInput); + + auto emptyLot = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + emptyLot.auctionLotItems = Array{}; + EXPECT_EQ(invokeRejectedPublicAuction(emptyLot).errorCode, NOST::EAuctionError::InvalidInput); + + auto negativeQuantity = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + negativeQuantity.auctionLotItems = ContractTestingNOST::makeSingleLot(assetA, -1); + EXPECT_EQ(invokeRejectedPublicAuction(negativeQuantity).errorCode, NOST::EAuctionError::InvalidInput); + + auto zeroDuration = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + zeroDuration.durationDays = 0; + EXPECT_EQ(invokeRejectedPublicAuction(zeroDuration).errorCode, NOST::EAuctionError::InvalidInput); + + auto tooLongDuration = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + tooLongDuration.durationDays = NOST_AUCTION_MAX_DURATION_DAYS + 1; + EXPECT_EQ(invokeRejectedPublicAuction(tooLongDuration).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidType = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + invalidType.auctionType = 99; + EXPECT_EQ(invokeRejectedPublicAuction(invalidType).errorCode, NOST::EAuctionError::InvalidAuctionType); + + auto invalidVisibility = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + invalidVisibility.auctionVisibility = 99; + EXPECT_EQ(invokeRejectedPublicAuction(invalidVisibility).errorCode, NOST::EAuctionError::InvalidVisibility); + + auto partiallyEmptyLot = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + partiallyEmptyLot.auctionLotItems = ContractTestingNOST::makeSingleLot(Asset{}, 1); + EXPECT_EQ(invokeRejectedPublicAuction(partiallyEmptyLot).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidBatchBuyNow = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + invalidBatchBuyNow.buyNowPrice = 100; + EXPECT_EQ(invokeRejectedPublicAuction(invalidBatchBuyNow).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidStandardIncrement = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); + invalidStandardIncrement.minimumBidIncrement = 0; + EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardIncrement).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidStandardLowInitial = ContractTestingNOST::makeStandardAuctionInput( + ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE - 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_BID_INCREMENT); + EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardLowInitial).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidStandardLowSale = ContractTestingNOST::makeStandardAuctionInput( + ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE - 1, NOST_STANDARD_MIN_BID_INCREMENT); + EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardLowSale).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidStandardLowIncrement = ContractTestingNOST::makeStandardAuctionInput( + ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_BID_INCREMENT - 1); + EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardLowIncrement).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidStandardPrice = ContractTestingNOST::makeStandardAuctionInput( + ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE + 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_BID_INCREMENT); + EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardPrice).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidStandardSalePrice = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); + invalidStandardSalePrice.salePrice = 0; + EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardSalePrice).errorCode, NOST::EAuctionError::InvalidInput); + + auto invalidStandardBuyNow = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE, + NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT, + NOST_STANDARD_MIN_BID_INCREMENT, NOST_STANDARD_MIN_PRICE - 1); + EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardBuyNow).errorCode, NOST::EAuctionError::InvalidInput); + + auto privateWithoutGate = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + privateWithoutGate.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + EXPECT_EQ(invokeRejectedPrivateAuction(privateWithoutGate).errorCode, NOST::EAuctionError::InvalidInput); + + auto zeroAccessQuantity = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + zeroAccessQuantity.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + zeroAccessQuantity.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAsset, 0}}); + EXPECT_EQ(invokeRejectedPrivateAuction(zeroAccessQuantity).errorCode, NOST::EAuctionError::InvalidInput); + + auto negativeAccessQuantity = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + negativeAccessQuantity.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + negativeAccessQuantity.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAsset, -1}}); + EXPECT_EQ(invokeRejectedPrivateAuction(negativeAccessQuantity).errorCode, NOST::EAuctionError::InvalidInput); + + auto partiallyEmptyAccessAsset = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + partiallyEmptyAccessAsset.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + partiallyEmptyAccessAsset.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{Asset{}, 1}}); + EXPECT_EQ(invokeRejectedPrivateAuction(partiallyEmptyAccessAsset).errorCode, NOST::EAuctionError::InvalidInput); + + EXPECT_EQ(getBalance(seller), sellerBalanceBeforeInvalidCalls); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBalanceBeforeInvalidCalls); + EXPECT_EQ(nostromo.managedShares(assetA, seller), 5); + EXPECT_EQ(nostromo.getLatestAuctionIndex().found, 0); + EXPECT_EQ(nostromo.getContractStats().stats.totalAuctionsCreated, 0ULL); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 0ULL); +} + +TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientAssetBalanceAndPauseAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(61, 62, 63, 64); + const uint64 assetName = assetNameFromString("PRIFEE"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 4), 4); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 4), 4); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 4, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({id(1, 1, 1, 1)}); + + const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE - 1); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::InsufficientFunds); + EXPECT_EQ(nostromo.managedShares(asset, seller), 4); + } + + { + ContractTestingNOST nostromo; + const id seller(71, 72, 73, 74); + const uint64 assetName = assetNameFromString("BALLOW"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 3, 10); + const auto output = nostromo.createAuction(seller, input); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::InsufficientAssetBalance); + EXPECT_EQ(nostromo.managedShares(asset, seller), 2); + } + + { + ContractTestingNOST nostromo; + const id seller(81, 82, 83, 84); + const uint64 assetName = assetNameFromString("PAUSEA"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 3, 10); + nostromo.setNow(2026, 1, 7, 11, 40, 0); + nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + + const auto output = nostromo.createAuction(seller, input); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::AuctionPaused); + EXPECT_EQ(output.auctionIndex, 0ULL); + EXPECT_EQ(nostromo.managedShares(asset, seller), 3); + } + + { + ContractTestingNOST nostromo; + const id seller(85, 86, 87, 88); + const uint64 assetName = assetNameFromString("BOOTPA"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 3, 10); + nostromo.setNow(2022, 4, 13, 12, 0, 0); + nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + + const auto output = nostromo.createAuction(seller, input); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::AuctionPaused); + EXPECT_EQ(output.auctionIndex, 0ULL); + EXPECT_EQ(nostromo.managedShares(asset, seller), 3); + } +} + +TEST(ContractNostromoAuction, CreateAuctionRejectsWhenAuctionStorageIsFullAuction) +{ + ContractTestingNOST nostromo; + const id seller(87, 88, 89, 90); + const uint64 assetName = assetNameFromString("STOFUL"); + const Asset asset{seller, assetName}; + + for (uint64 index = 0; index < NOST_AUCTION_NUM; ++index) + { + NOST::AuctionData auction{}; + auction.core.auctionIndex = index; + auction.core.seller = seller; + auction.core.status = NOST::EAuctionStatus::Active; + ASSERT_NE(nostromo.stateData().auctionList.set(auction.core.auctionIndex, auction), NULL_INDEX); + } + ASSERT_EQ(nostromo.stateData().auctionList.population(), NOST_AUCTION_NUM); + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto output = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10)); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::StorageFull); + EXPECT_EQ(output.auctionIndex, 0ULL); + EXPECT_EQ(nostromo.managedShares(asset, seller), 1); +} + +TEST(ContractNostromoAuction, CreateAuctionRejectsWhenAuctionIndexIsExhaustedAuction) +{ + ContractTestingNOST nostromo; + const id seller(89, 90, 91, 92); + const uint64 assetName = assetNameFromString("IDXMAX"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + nostromo.stateData().totalAuctionsCreated = UINT64_MAX; + + const auto output = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10)); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::AuctionIndexExhausted); + EXPECT_EQ(output.auctionIndex, 0ULL); + EXPECT_EQ(nostromo.stateData().totalAuctionsCreated, UINT64_MAX); + EXPECT_EQ(nostromo.stateData().auctionList.population(), 0ULL); + EXPECT_EQ(nostromo.managedShares(asset, seller), 1); +} + +TEST(ContractNostromoAuction, PlaceBidRejectsWhenParticipantStorageIsFullAuction) +{ + ContractTestingNOST nostromo; + const id seller(91, 92, 93, 94); + const id bidder(95, 96, 97, 98); + const uint64 assetName = assetNameFromString("PARFUL"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + for (uint64 index = 0; index < NOST_AUCTION_PARTICIPANT_NUM; ++index) + { + NOST::AuctionParticipantData participant{}; + participant.auctionIndex = index + 100000ULL; + participant.bidIndex = index; + participant.participant = id(14000 + index, 15000 + index, 16000 + index, 17000 + index); + participant.bidAmount = 1; + participant.requestedQuantity = 1; + participant.isUsed = 1; + participant.isActive = 1; + nostromo.stateData().participants.set(index, participant); + } + uint64 usedParticipantCount = 0; + for (uint64 index = 0; index < NOST_AUCTION_PARTICIPANT_NUM; ++index) + { + if (nostromo.stateData().participants.get(index).isUsed) + { + ++usedParticipantCount; + } + } + ASSERT_EQ(usedParticipantCount, NOST_AUCTION_PARTICIPANT_NUM); + + const auto output = nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, 1, 10); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::StorageFull); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.highestBidAmount, 0ULL); + EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidder).found, 0); +} + +TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAuction) +{ + ContractTestingNOST nostromo; + const id seller(91, 92, 93, 94); + const id bidderA(95, 96, 97, 98); + const id bidderB(99, 100, 101, 102); + const id bidderC(103, 104, 105, 106); + const uint64 assetName = assetNameFromString("BIDBAT"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 6), 6); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 6), 6); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 6, 10)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto sellerBid = nostromo.placeBid(seller, createOutput.auctionIndex, 1, 12, 12); + EXPECT_EQ(sellerBid.errorCode, NOST::EAuctionError::Forbidden); + + const auto missingAuction = nostromo.placeBid(bidderA, 700, 1, 12, 12); + EXPECT_EQ(missingAuction.errorCode, NOST::EAuctionError::AuctionNotFound); + + const auto zeroQuantity = nostromo.placeBid(bidderA, createOutput.auctionIndex, 0, 12, 12); + EXPECT_EQ(zeroQuantity.errorCode, NOST::EAuctionError::InvalidInput); + + const auto zeroBid = nostromo.placeBid(bidderA, createOutput.auctionIndex, 1, 0, 1); + EXPECT_EQ(zeroBid.errorCode, NOST::EAuctionError::InvalidInput); + + const auto tooLow = nostromo.placeBid(bidderA, createOutput.auctionIndex, 1, 9, 9); + EXPECT_EQ(tooLow.errorCode, NOST::EAuctionError::BidTooLow); + + const auto insufficientFunds = nostromo.placeBid(bidderA, createOutput.auctionIndex, 2, 12, 23); + EXPECT_EQ(insufficientFunds.errorCode, NOST::EAuctionError::InsufficientFunds); + + const auto bidA1 = nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 2, 20); + const auto bidB = nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 3, 15); + ASSERT_EQ(bidA1.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(bidB.errorCode, NOST::EAuctionError::Success); + + auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.highestBidder, bidderA); + EXPECT_EQ(auction.core.highestBidPrice, 20ULL); + EXPECT_EQ(auction.core.highestBidAmount, 40ULL); + + const auto bidA2 = nostromo.placeBid(bidderA, createOutput.auctionIndex, 2, 14, 28); + EXPECT_EQ(bidA2.errorCode, NOST::EAuctionError::QuantityUnavailable); + EXPECT_EQ(bidA2.refundedAmount, 28ULL); + + auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.highestBidder, bidderA); + EXPECT_EQ(auction.core.highestBidPrice, 20ULL); + EXPECT_EQ(auction.core.highestBidAmount, 40ULL); + + const auto participantA = nostromo.getParticipant(createOutput.auctionIndex, bidderA); + ASSERT_EQ(participantA.found, 1); + EXPECT_EQ(participantA.participantData.escrowedAmount, 40ULL); + EXPECT_EQ(participantA.participantData.bidAmount, 20ULL); + + nostromo.setNow(2026, 1, 2, 9, 0, 1); + const auto closed = nostromo.placeBid(bidderC, createOutput.auctionIndex, 1, 30, 30); + EXPECT_EQ(closed.errorCode, NOST::EAuctionError::AuctionClosed); +} + +TEST(ContractNostromoAuction, PlaceBatchBidExtendsAuctionNearEndAuction) +{ + ContractTestingNOST nostromo; + const id seller(111, 112, 113, 114); + const id bidder(115, 116, 117, 118); + const uint64 assetName = assetNameFromString("BIDEXT"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + nostromo.setNow(2026, 1, 2, 8, 56, 30); + const auto bidOutput = nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, 1, 15); + ASSERT_EQ(bidOutput.errorCode, NOST::EAuctionError::Success); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.auctionDurationSeconds, NOST_SECONDS_PER_DAY + NOST_AUCTION_EXTENSION_SECONDS); +} + +TEST(ContractNostromoAuction, BatchBidAvailabilityRejectsOversizedTailAuction) +{ + ContractTestingNOST nostromo; + const id seller(601, 602, 603, 604); + const id bidderA(605, 606, 607, 608); + const id bidderB(609, 610, 611, 612); + const Asset asset{seller, assetNameFromString("BAVAIL")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 8, 40).errorCode, NOST::EAuctionError::Success); + auto availability = nostromo.getBatchAvailability(createOutput.auctionIndex); + EXPECT_EQ(availability.found, 1); + EXPECT_EQ(availability.isAcceptingBids, 1); + EXPECT_EQ(availability.minimumBidPrice, 2ULL); + EXPECT_EQ(availability.availableQuantity, 2ULL); + + const auto oversized = nostromo.placeBid(bidderB, createOutput.auctionIndex, 3, 20, 6); + EXPECT_EQ(oversized.errorCode, NOST::EAuctionError::QuantityUnavailable); + EXPECT_EQ(oversized.refundedAmount, 6ULL); + EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidderB).found, 0); + + const auto exactTail = nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 2, 20); + EXPECT_EQ(exactTail.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidderB).participantData.requestedQuantity, 2ULL); +} + +TEST(ContractNostromoAuction, BatchCoveredLotRequiresHigherPriceAuction) +{ + ContractTestingNOST nostromo; + const id seller(613, 614, 615, 616); + const id bidderA(617, 618, 619, 620); + const id bidderB(621, 622, 623, 624); + const Asset asset{seller, assetNameFromString("BCOVER")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 10, 30).errorCode, NOST::EAuctionError::Success); + auto availability = nostromo.getBatchAvailability(createOutput.auctionIndex); + EXPECT_EQ(availability.minimumBidPrice, 31ULL); + EXPECT_EQ(availability.availableQuantity, 0ULL); + EXPECT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, 20, 2).errorCode, NOST::EAuctionError::BidTooLow); + EXPECT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, 30, 3).errorCode, NOST::EAuctionError::BidTooLow); + + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 3, 40).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidderA).participantData.requestedQuantity, 7ULL); + EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidderB).participantData.requestedQuantity, 3ULL); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidderA), 7); + EXPECT_EQ(nostromo.managedShares(asset, bidderB), 3); + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); +} + +TEST(ContractNostromoAuction, BatchMinimumPurchaseTailAndSameBidderDisplacementAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(625, 626, 627, 628); + const id bidderA(629, 630, 631, 632); + const id bidderB(633, 634, 635, 636); + const Asset asset{seller, assetNameFromString("BTAILM")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2); + input.minimumPurchaseQuantity = 3; + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 8, 40).errorCode, NOST::EAuctionError::Success); + auto availability = nostromo.getBatchAvailability(createOutput.auctionIndex); + EXPECT_EQ(availability.minimumBidPrice, 41ULL); + EXPECT_EQ(availability.availableQuantity, 0ULL); + EXPECT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 2, 4, 8).errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 3, 40, 12).errorCode, NOST::EAuctionError::BidTooLow); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 3, 50).errorCode, NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidderA), 7); + EXPECT_EQ(nostromo.managedShares(asset, bidderB), 3); + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); + } + + { + ContractTestingNOST nostromo; + const id seller(637, 638, 639, 640); + const id bidderA(641, 642, 643, 644); + const Asset asset{seller, assetNameFromString("BSAMEB")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2); + input.minimumPurchaseQuantity = 3; + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 10, 30).errorCode, NOST::EAuctionError::Success); + const auto improved = nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 8, 40); + EXPECT_EQ(improved.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(improved.refundedAmount, 300ULL); + + const auto participants = nostromo.getAuctionParticipants(createOutput.auctionIndex, 0, 64); + ASSERT_EQ(participants.totalCount, 2ULL); + uint64 quantityAtThirty = 0; + uint64 quantityAtForty = 0; + for (uint64 index = 0; index < participants.returnedCount; ++index) + { + if (participants.participants.get(index).bidAmount == 30) { - return tmpInvestedList.get(i).investedAmount; + quantityAtThirty = participants.participants.get(index).requestedQuantity; } + if (participants.participants.get(index).bidAmount == 40) + { + quantityAtForty = participants.participants.get(index).requestedQuantity; + } + } + EXPECT_EQ(quantityAtThirty, 0ULL); + EXPECT_EQ(quantityAtForty, 8ULL); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidderA), 8); + EXPECT_EQ(nostromo.managedShares(asset, seller), 2); + } +} + +TEST(ContractNostromoAuction, BatchDisplacementKeepsExactMinimumResidualAuction) +{ + ContractTestingNOST nostromo; + const id seller(6601, 6602, 6603, 6604); + const id bidderA(6611, 6612, 6613, 6614); + const id bidderB(6621, 6622, 6623, 6624); + const Asset asset{seller, assetNameFromString("BMINEX")}; + + ASSERT_EQ(nostromo.issueAsset(seller, asset.assetName, 10), 10); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2); + input.minimumPurchaseQuantity = 3; + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 6, 30).errorCode, NOST::EAuctionError::Success); + const auto higherBid = nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 7, 40); + ASSERT_EQ(higherBid.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(higherBid.refundedAmount, 90ULL); + EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidderA).participantData.requestedQuantity, 3ULL); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidderA), 3); + EXPECT_EQ(nostromo.managedShares(asset, bidderB), 7); + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); +} + +TEST(ContractNostromoAuction, DeterministicBatchAllocationPropertiesAuction) +{ + uint64 generatorState = 0x9E3779B97F4A7C15ULL; + // Unsigned wraparound is intentional: this fixed LCG makes boundary-heavy scenarios reproducible. + for (uint64 scenario = 0; scenario < 12; ++scenario) + { + SCOPED_TRACE(::testing::Message() << "scenario=" << scenario); + ContractTestingNOST nostromo; + generatorState = generatorState * 6364136223846793005ULL + 1442695040888963407ULL; + const uint64 quantityForSale = 3ULL + generatorState % 6ULL; + generatorState = generatorState * 6364136223846793005ULL + 1442695040888963407ULL; + const uint64 minimumPurchaseQuantity = 1ULL + generatorState % quantityForSale; + const id seller(7000 + scenario, 7100 + scenario, 7200 + scenario, 7300 + scenario); + const Asset asset{seller, assetNameFromString("PROPBA")}; + + ASSERT_EQ(nostromo.issueAsset(seller, asset.assetName, static_cast(quantityForSale)), static_cast(quantityForSale)); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, static_cast(quantityForSale)), + static_cast(quantityForSale)); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, static_cast(quantityForSale), 10); + input.minimumPurchaseQuantity = minimumPurchaseQuantity; + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + for (uint64 bidIndex = 0; bidIndex < 6; ++bidIndex) + { + SCOPED_TRACE(::testing::Message() << "bidIndex=" << bidIndex); + generatorState = generatorState * 6364136223846793005ULL + 1442695040888963407ULL; + const uint64 bidQuantity = minimumPurchaseQuantity + generatorState % (quantityForSale - minimumPurchaseQuantity + 1ULL); + const uint64 bidPrice = 20ULL + bidIndex * 10ULL; + const id bidder(8000 + scenario * 10 + bidIndex, 9000 + bidIndex, 10000 + scenario, 11000 + bidIndex); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, bidQuantity, bidPrice).errorCode, + NOST::EAuctionError::Success); } - return 0; - } - uint64 getEpochRevenue() - { - return epochRevenue; - } - void totalRaisedFundChecker(uint32 indexOfFundraising, uint64 raisedFund, uint64 assetName) - { - EXPECT_EQ(raisedFund, fundaraisings.get(indexOfFundraising).raisedFunds); - - if (fundaraisings.get(indexOfFundraising).isCreatedToken) - { - Asset assetInfo; - assetInfo.assetName = assetName; - assetInfo.issuer = id(NOST_CONTRACT_INDEX, 0, 0, 0); - EXPECT_EQ(numberOfShares(assetInfo), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).supplyOfToken); - EXPECT_EQ(numberOfPossessedShares(assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator, projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).supplyOfToken - fundaraisings.get(indexOfFundraising).soldAmount); - } - } - void endEpochSucceedFundraisingChecker(id creator, uint32 indexOfFundraising, uint64 totalInvestedFund, uint64 originalCreatorBalance, uint64 assetName) - { - EXPECT_EQ(numberOfPossessedShares(assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), creator, creator, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).supplyOfToken - div(totalInvestedFund, fundaraisings.get(indexOfFundraising).tokenPrice)); - EXPECT_EQ(fundaraisings.get(indexOfFundraising).raisedFunds, 0); - } - void endEpochFailedFundraisingChecker(uint32 indexOfFundraising) - { - EXPECT_EQ(fundaraisings.get(indexOfFundraising).raisedFunds, 0); - } - void endEpochVoteStatusClearChecker() - { - id userId; - uint64 tierLevel; - uint64 idx = users.nextElementIndex(NULL_INDEX); - uint32 numberOfProject; - Array votedList; - while (idx != NULL_INDEX) + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + const auto participants = nostromo.getAuctionParticipants(createOutput.auctionIndex, 0, NOST_AUCTION_GETTER_PAGE_SIZE); + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + uint64 allocatedTotal = 0; + for (uint64 participantIndex = 0; participantIndex < participants.returnedCount; ++participantIndex) { - userId = users.key(idx); - tierLevel = users.value(idx); + const auto participant = participants.participants.get(participantIndex); + EXPECT_LE(participant.allocatedQuantity, participant.requestedQuantity); + EXPECT_TRUE(participant.allocatedQuantity == 0 || participant.allocatedQuantity >= minimumPurchaseQuantity); + allocatedTotal += participant.allocatedQuantity; + } + EXPECT_EQ(allocatedTotal, auction.core.allocatedQuantity); + EXPECT_LE(allocatedTotal, quantityForSale); + EXPECT_EQ(nostromo.managedShares(asset, seller), static_cast(quantityForSale - allocatedTotal)); + } +} + +TEST(ContractNostromoAuction, BatchAvailabilityGetterStatesAuction) +{ + ContractTestingNOST nostromo; + const id batchSeller(645, 646, 647, 648); + const id standardSeller(649, 650, 651, 652); + const id bidder(653, 654, 655, 656); + const Asset closedBatchAsset{batchSeller, assetNameFromString("BGETCL")}; + const Asset maxBatchAsset{batchSeller, assetNameFromString("BGETMX")}; + const Asset standardAsset{standardSeller, assetNameFromString("BGETST")}; + + EXPECT_EQ(nostromo.getBatchAvailability(999).found, 0); + + EXPECT_EQ(nostromo.issueAsset(standardSeller, standardAsset.assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(standardSeller, standardAsset, 1), 1); + const auto standardCreate = + nostromo.createAuction(standardSeller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(standardAsset, 1))); + ASSERT_EQ(standardCreate.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getBatchAvailability(standardCreate.auctionIndex).found, 1); + EXPECT_EQ(nostromo.getBatchAvailability(standardCreate.auctionIndex).isAcceptingBids, 0); + + EXPECT_EQ(nostromo.issueAsset(batchSeller, closedBatchAsset.assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(batchSeller, closedBatchAsset, 1), 1); + const auto closedBatchCreate = nostromo.createAuction(batchSeller, ContractTestingNOST::makeBatchAuctionInput(closedBatchAsset, 1, 2)); + ASSERT_EQ(closedBatchCreate.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, closedBatchCreate.auctionIndex, 1, 2).errorCode, NOST::EAuctionError::Success); + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.getBatchAvailability(closedBatchCreate.auctionIndex).found, 1); + EXPECT_EQ(nostromo.getBatchAvailability(closedBatchCreate.auctionIndex).isAcceptingBids, 0); + + EXPECT_EQ(nostromo.issueAsset(batchSeller, maxBatchAsset.assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(batchSeller, maxBatchAsset, 1), 1); + const auto batchCreate = nostromo.createAuction(batchSeller, ContractTestingNOST::makeBatchAuctionInput(maxBatchAsset, 1, 2)); + ASSERT_EQ(batchCreate.errorCode, NOST::EAuctionError::Success); + NOST::AuctionParticipantData maxPriceBid{}; + maxPriceBid.auctionIndex = batchCreate.auctionIndex; + maxPriceBid.bidIndex = 0; + maxPriceBid.participant = bidder; + maxPriceBid.bidAmount = UINT64_MAX; + maxPriceBid.requestedQuantity = 1; + maxPriceBid.escrowedAmount = 1; + maxPriceBid.isUsed = 1; + maxPriceBid.isActive = 1; + maxPriceBid.isWinningBid = 1; + nostromo.stateData().participants.set(0, maxPriceBid); + EXPECT_EQ(nostromo.getBatchAvailability(batchCreate.auctionIndex).isAcceptingBids, 0); +} + +TEST(ContractNostromoAuction, PlaceStandardBidValidatesRefundsAndPauseAuction) +{ + ContractTestingNOST nostromo; + const id seller(121, 122, 123, 124); + const id bidderA(125, 126, 127, 128); + const id bidderB(129, 130, 131, 132); + const uint64 assetName = assetNameFromString("STDVAL"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = nostromo.createAuction( + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, + NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto sellerBid = nostromo.placeBid(seller, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE); + EXPECT_EQ(sellerBid.errorCode, NOST::EAuctionError::Forbidden); + + const auto lowStart = nostromo.placeBid(bidderA, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE - 1, NOST_STANDARD_MIN_PRICE - 1); + EXPECT_EQ(lowStart.errorCode, NOST::EAuctionError::BidTooLow); + + const auto openingBid = nostromo.placeBid(bidderA, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE); + ASSERT_EQ(openingBid.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(openingBid.escrowedAmount, NOST_STANDARD_MIN_PRICE); + + const auto lowIncrement = nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT - 1, + NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT - 1); + EXPECT_EQ(lowIncrement.errorCode, NOST::EAuctionError::BidTooLow); + + const auto outbid = nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT, + NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT); + ASSERT_EQ(outbid.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(outbid.refundedAmount, NOST_STANDARD_MIN_PRICE); + + const auto bidderAState = nostromo.getParticipant(createOutput.auctionIndex, bidderA); + const auto bidderBState = nostromo.getParticipant(createOutput.auctionIndex, bidderB); + ASSERT_EQ(bidderAState.found, 1); + ASSERT_EQ(bidderBState.found, 1); + EXPECT_EQ(bidderAState.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(bidderAState.participantData.isWinningBid, 0u); + EXPECT_EQ(bidderBState.participantData.escrowedAmount, NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT); + EXPECT_EQ(bidderBState.participantData.isWinningBid, 1u); + + const auto bidderBImprove = + nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 30000ULL, NOST_STANDARD_MIN_PRICE + 30000ULL); + EXPECT_EQ(bidderBImprove.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(bidderBImprove.refundedAmount, NOST_STANDARD_MIN_PRICE + NOST_STANDARD_MIN_BID_INCREMENT); + EXPECT_EQ(bidderBImprove.escrowedAmount, NOST_STANDARD_MIN_PRICE + 30000ULL); + + nostromo.beginEpoch(); + EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + const auto pausedBid = nostromo.placeBid(id(133, 134, 135, 136), createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 40000ULL, + NOST_STANDARD_MIN_PRICE + 40000ULL); + EXPECT_EQ(pausedBid.errorCode, NOST::EAuctionError::AuctionPaused); + nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + + const auto resumedBid = nostromo.placeBid(id(137, 138, 139, 140), createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 40000ULL, + NOST_STANDARD_MIN_PRICE + 40000ULL); + EXPECT_EQ(resumedBid.errorCode, NOST::EAuctionError::Success); + + nostromo.setNow(2022, 4, 13, 12, 0, 0); + nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + + const auto bootstrapPausedBid = nostromo.placeBid(id(141, 142, 143, 144), createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 50000ULL, + NOST_STANDARD_MIN_PRICE + 50000ULL); + EXPECT_EQ(bootstrapPausedBid.errorCode, NOST::EAuctionError::AuctionPaused); +} + +TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(141, 142, 143, 144); + const id allowed(145, 146, 147, 148); + const id denied(149, 150, 151, 152); + const uint64 assetName = assetNameFromString("PRIBID"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); + + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 3, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowed}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + EXPECT_EQ(nostromo.placeBid(denied, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::PrivateAuctionAccessDenied); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(allowed, createOutput.auctionIndex, 1, 12).errorCode, NOST::EAuctionError::Success); + } + + { + ContractTestingNOST nostromo; + const id seller(153, 154, 155, 156); + const id gateIssuerA(157, 158, 159, 160); + const id gateIssuerB(161, 162, 163, 164); + const id belowThresholdBidder(165, 166, 167, 168); + const id exactThresholdBidder(169, 170, 171, 172); + const id alternateAssetBidder(173, 174, 175, 176); + const uint64 saleAssetName = assetNameFromString("PRIACS"); + const Asset saleAsset{seller, saleAssetName}; + const Asset accessAssetA{gateIssuerA, assetNameFromString("PRIAGA")}; + const Asset accessAssetB{gateIssuerB, assetNameFromString("PRIAGB")}; + + EXPECT_EQ(nostromo.issueAsset(seller, saleAssetName, 3), 3); + EXPECT_EQ(nostromo.issueAsset(gateIssuerA, accessAssetA.assetName, 5), 5); + EXPECT_EQ(nostromo.issueAsset(gateIssuerB, accessAssetB.assetName, 5), 5); + EXPECT_EQ(nostromo.transferAsset(gateIssuerA, belowThresholdBidder, accessAssetA, 2), 2); + EXPECT_EQ(nostromo.transferAsset(gateIssuerA, exactThresholdBidder, accessAssetA, 3), 3); + EXPECT_EQ(nostromo.transferAsset(gateIssuerB, alternateAssetBidder, accessAssetB, 5), 5); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, saleAsset, 3), 3); + + auto input = ContractTestingNOST::makeBatchAuctionInput(saleAsset, 3, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.requiredAccessAssets = + ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAssetA, 3}, NOST::AuctionAssetEntry{accessAssetB, 5}}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + EXPECT_EQ(nostromo.placeBid(belowThresholdBidder, createOutput.auctionIndex, 1, 12, 12).errorCode, + NOST::EAuctionError::PrivateAuctionAccessDenied); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(exactThresholdBidder, createOutput.auctionIndex, 1, 12).errorCode, + NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(alternateAssetBidder, createOutput.auctionIndex, 1, 13).errorCode, + NOST::EAuctionError::Success); + } +} + +TEST(ContractNostromoAuction, PrivateAuctionCombinedAccessUsesInclusiveOrAuction) +{ + ContractTestingNOST nostromo; + const id seller(177, 178, 179, 180); + const id gateIssuer(181, 182, 183, 184); + const id walletOnlyBidder(185, 186, 187, 188); + const id assetOnlyBidder(189, 190, 191, 192); + const id bothBidder(193, 194, 195, 196); + const id deniedBidder(197, 198, 199, 200); + const uint64 saleAssetName = assetNameFromString("PRIORA"); + const Asset saleAsset{seller, saleAssetName}; + const Asset accessAsset{gateIssuer, assetNameFromString("PRIORG")}; + + EXPECT_EQ(nostromo.issueAsset(seller, saleAssetName, 3), 3); + EXPECT_EQ(nostromo.issueAsset(gateIssuer, accessAsset.assetName, 2), 2); + EXPECT_EQ(nostromo.transferAsset(gateIssuer, assetOnlyBidder, accessAsset, 1), 1); + EXPECT_EQ(nostromo.transferAsset(gateIssuer, bothBidder, accessAsset, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, saleAsset, 3), 3); + + auto input = ContractTestingNOST::makeBatchAuctionInput(saleAsset, 3, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({walletOnlyBidder, bothBidder}); + input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAsset, 1}}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + const auto auctionOutput = nostromo.getAuction(createOutput.auctionIndex); + EXPECT_EQ(auctionOutput.auction.allowedBidderWalletCount, 2U); + EXPECT_EQ(auctionOutput.auction.requiredAccessAssetCount, 1U); + + nostromo.seedUser(deniedBidder, 100); + const sint64 deniedBalanceBefore = getBalance(deniedBidder); + const sint64 contractBalanceBeforeDeniedBid = getBalance(NOST_CONTRACT_ID); + EXPECT_EQ(nostromo.placeBidWithFundedReward(deniedBidder, createOutput.auctionIndex, 1, 12, 12).errorCode, + NOST::EAuctionError::PrivateAuctionAccessDenied); + EXPECT_EQ(getBalance(deniedBidder), deniedBalanceBefore); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBalanceBeforeDeniedBid); + EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, deniedBidder).found, 0); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(walletOnlyBidder, createOutput.auctionIndex, 1, 12).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(assetOnlyBidder, createOutput.auctionIndex, 1, 13).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(bothBidder, createOutput.auctionIndex, 1, 14).errorCode, NOST::EAuctionError::Success); +} + +TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) +{ + ContractTestingNOST nostromo; + const id seller(171, 172, 173, 174); + const id bidder(175, 176, 177, 178); + const uint64 assetName = assetNameFromString("BUYNWA"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); + + auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 3), NOST_STANDARD_MIN_PRICE, + NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT, + NOST_STANDARD_MIN_PRICE + 800000ULL); + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + const sint64 sellerBalanceBefore = getBalance(seller); + + const auto bidOutput = + nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 800000ULL, NOST_STANDARD_MIN_PRICE + 800000ULL); + ASSERT_EQ(bidOutput.errorCode, NOST::EAuctionError::Success); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + const auto participant = nostromo.getParticipant(createOutput.auctionIndex, bidder); + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.core.allocatedQuantity, 1ULL); + ASSERT_EQ(participant.found, 1); + EXPECT_EQ(participant.participantData.allocatedQuantity, 1ULL); + EXPECT_EQ(participant.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(participant.participantData.isWinningBid, 1u); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 3); + EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, 1683000ULL); +} + +TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialFillAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(181, 182, 183, 184); + const id bidderA(185, 186, 187, 188); + const id bidderB(189, 190, 191, 192); + const id bidderC(193, 194, 195, 196); + const uint64 assetName = assetNameFromString("BATFIN"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 4), 4); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 4), 4); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 4, 10)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderA, createOutput.auctionIndex, 3, 15).errorCode, NOST::EAuctionError::Success); + nostromo.setNow(2026, 1, 1, 9, 0, 1); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 1, 15).errorCode, NOST::EAuctionError::Success); + nostromo.setNow(2026, 1, 1, 9, 0, 2); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderC, createOutput.auctionIndex, 2, 20).errorCode, NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + const auto participantA = nostromo.getParticipant(createOutput.auctionIndex, bidderA); + const auto participantB = nostromo.getParticipant(createOutput.auctionIndex, bidderB); + const auto participantC = nostromo.getParticipant(createOutput.auctionIndex, bidderC); + + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.core.allocatedQuantity, 4ULL); + ASSERT_EQ(participantA.found, 1); + ASSERT_EQ(participantB.found, 1); + ASSERT_EQ(participantC.found, 1); + EXPECT_EQ(participantC.participantData.allocatedQuantity, 2ULL); + EXPECT_EQ(participantA.participantData.allocatedQuantity, 2ULL); + EXPECT_EQ(participantB.participantData.allocatedQuantity, 0ULL); + EXPECT_EQ(participantA.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(participantB.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(participantC.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(participantA.participantData.isWinningBid, 1u); + EXPECT_EQ(participantB.participantData.isWinningBid, 0u); + EXPECT_EQ(participantC.participantData.isWinningBid, 1u); + EXPECT_EQ(nostromo.managedShares(asset, bidderA), 2); + EXPECT_EQ(nostromo.managedShares(asset, bidderB), 0); + EXPECT_EQ(nostromo.managedShares(asset, bidderC), 2); + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); + } + + { + ContractTestingNOST nostromo; + const id seller(197, 198, 199, 200); + const id bidder(201, 202, 203, 204); + const uint64 assetName = assetNameFromString("BATRET"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 5), 5); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 5), 5); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 5, 10)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, 2, 12).errorCode, NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.core.allocatedQuantity, 2ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 2); + EXPECT_EQ(nostromo.managedShares(asset, seller), 3); + } + + { + ContractTestingNOST nostromo; + const id seller(313, 314, 315, 316); + const id firstBidder(317, 318, 319, 320); + const id secondBidder(321, 322, 323, 324); + const Asset asset{seller, assetNameFromString("BATPRTL")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 15), 15); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 15), 15); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 15, 10); + input.minimumPurchaseQuantity = 10; + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + const sint64 sellerBalanceBefore = getBalance(seller); + nostromo.seedUser(firstBidder, 291); + nostromo.seedUser(secondBidder, 150); + const sint64 secondBidderBalanceBefore = getBalance(secondBidder); + + ASSERT_EQ(nostromo.placeBatchBidWithFundedRequiredReward(firstBidder, createOutput.auctionIndex, 10, 20).errorCode, + NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBidWithFundedReward(secondBidder, createOutput.auctionIndex, 10, 15, 150).errorCode, NOST::EAuctionError::BidTooLow); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto firstParticipant = nostromo.getParticipant(createOutput.auctionIndex, firstBidder); + const auto secondParticipant = nostromo.getParticipant(createOutput.auctionIndex, secondBidder); + ASSERT_EQ(firstParticipant.found, 1); + ASSERT_EQ(secondParticipant.found, 0); + EXPECT_EQ(firstParticipant.participantData.allocatedQuantity, 10ULL); + EXPECT_EQ(nostromo.managedShares(asset, firstBidder), 10); + EXPECT_EQ(nostromo.managedShares(asset, secondBidder), 0); + EXPECT_EQ(nostromo.managedShares(asset, seller), 5); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.allocatedQuantity, 10ULL); + EXPECT_EQ(getBalance(secondBidder), secondBidderBalanceBefore); + EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, 187ULL); + } + + { + ContractTestingNOST nostromo; + const id seller(325, 326, 327, 328); + const id firstBidder(329, 330, 331, 332); + const id partialBidder(333, 334, 335, 336); + const Asset asset{seller, assetNameFromString("BATPMIN")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 22), 22); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 22), 22); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 22, 10); + input.minimumPurchaseQuantity = 10; + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(firstBidder, createOutput.auctionIndex, 10, 22).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(partialBidder, createOutput.auctionIndex, 15, 22, 225).errorCode, NOST::EAuctionError::QuantityUnavailable); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto partialParticipant = nostromo.getParticipant(createOutput.auctionIndex, partialBidder); + ASSERT_EQ(partialParticipant.found, 0); + EXPECT_EQ(nostromo.managedShares(asset, partialBidder), 0); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.allocatedQuantity, 10ULL); + } +} + +TEST(ContractNostromoAuction, BatchFinalizationRefundsLosingBidsAndTieBreaksByBidTimeAuction) +{ + ContractTestingNOST nostromo; + const id seller(205, 206, 207, 208); + const id earlierBidder(209, 210, 211, 212); + const id laterBidder(213, 214, 215, 216); + const id higherBidder(217, 218, 219, 220); + const uint64 assetName = assetNameFromString("BATTIE"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 5)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + nostromo.seedUser(earlierBidder, 100); + nostromo.seedUser(laterBidder, 100); + nostromo.seedUser(higherBidder, 100); + const sint64 earlierBefore = getBalance(earlierBidder); + const sint64 laterBefore = getBalance(laterBidder); + const sint64 higherBefore = getBalance(higherBidder); + + ASSERT_EQ(nostromo.placeBatchBidWithFundedRequiredReward(earlierBidder, createOutput.auctionIndex, 1, 10).errorCode, + NOST::EAuctionError::Success); + nostromo.setNow(2026, 1, 1, 9, 0, 1); + ASSERT_EQ(nostromo.placeBatchBidWithFundedRequiredReward(laterBidder, createOutput.auctionIndex, 1, 10).errorCode, NOST::EAuctionError::Success); + nostromo.setNow(2026, 1, 1, 9, 0, 2); + ASSERT_EQ(nostromo.placeBatchBidWithFundedRequiredReward(higherBidder, createOutput.auctionIndex, 1, 11).errorCode, NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto earlier = nostromo.getParticipant(createOutput.auctionIndex, earlierBidder); + const auto later = nostromo.getParticipant(createOutput.auctionIndex, laterBidder); + const auto higher = nostromo.getParticipant(createOutput.auctionIndex, higherBidder); + ASSERT_EQ(earlier.found, 1); + ASSERT_EQ(later.found, 1); + ASSERT_EQ(higher.found, 1); + EXPECT_EQ(earlier.participantData.allocatedQuantity, 1ULL); + EXPECT_EQ(later.participantData.allocatedQuantity, 0ULL); + EXPECT_EQ(higher.participantData.allocatedQuantity, 1ULL); + EXPECT_EQ(earlier.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(later.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(higher.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(nostromo.managedShares(asset, earlierBidder), 1); + EXPECT_EQ(nostromo.managedShares(asset, laterBidder), 0); + EXPECT_EQ(nostromo.managedShares(asset, higherBidder), 1); + EXPECT_EQ(getBalance(earlierBidder), earlierBefore - 100); + EXPECT_EQ(getBalance(laterBidder), laterBefore - 90); + EXPECT_EQ(getBalance(higherBidder), higherBefore - 100); +} + +TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) +{ + ContractTestingNOST nostromo; + const id seller(211, 212, 213, 214); + const uint64 assetName = assetNameFromString("STDNOB"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(voteStatus.get(userId, votedList), 0); - EXPECT_EQ(numberOfVotedProject.get(userId, numberOfProject), 0); + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - idx = users.nextElementIndex(idx); + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.core.allocatedQuantity, 0ULL); + EXPECT_TRUE(isZero(auction.core.highestBidder)); + EXPECT_EQ(nostromo.managedShares(asset, seller), 1); +} + +TEST(ContractNostromoAuction, WeeklyPauseShiftsActiveAuctionDeadlineAuction) +{ + ContractTestingNOST nostromo; + const id seller(215, 216, 217, 218); + const uint64 assetName = assetNameFromString("PAUSHL"); + const Asset asset{seller, assetName}; + + nostromo.setNow(2026, 1, 6, 11, 40, 0); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + nostromo.setNow(2026, 1, 7, 11, 40, 0); + nostromo.advanceAndEndTick(0); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); + + nostromo.setNow(2026, 1, 7, 12, 0, 0); + nostromo.advanceAndEndTick(0); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); + + nostromo.setNow(2026, 1, 7, 12, 10, 1); + nostromo.advanceAndEndTick(0); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.managedShares(asset, seller), 1); +} + +TEST(ContractNostromoAuction, EndTickSkipsAuctionProcessingAtBootstrapTimeAuction) +{ + ContractTestingNOST nostromo; + const id seller(215, 216, 217, 218); + const uint64 assetName = assetNameFromString("BOOTTK"); + const Asset asset{seller, assetName}; + + nostromo.setNow(2022, 4, 12, 12, 0, 0); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + nostromo.setNow(2022, 4, 13, 12, 0, 0); + nostromo.advanceAndEndTick(0); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Active); + EXPECT_EQ(auction.core.allocatedQuantity, 0ULL); + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); +} + +TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) +{ + ContractTestingNOST nostromo; + const id seller(219, 220, 221, 222); + const id bidder(223, 224, 225, 226); + const uint64 assetName = assetNameFromString("PDSHFT"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = nostromo.createAuction( + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, + NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ( + nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 200000ULL, NOST_STANDARD_MIN_PRICE + 200000ULL).errorCode, + NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY) * 1000ULL); + auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + const auto originalSellerDecisionDeadline = auction.core.sellerDecisionDeadline; + ASSERT_EQ(auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(auction.core.sellerDecisionDeadline.getHour(), 9); + EXPECT_EQ(auction.core.sellerDecisionDeadline.getMinute(), 0); + EXPECT_EQ(auction.core.sellerDecisionDeadline.getSecond(), 0); + + nostromo.setNow(2026, 1, 9, 8, 59, 50); + nostromo.beginEpoch(); + const uint32 launchPauseTicksAfterBeginEpoch = nostromo.getTicksBeforeAuctionLaunch().ticks; + EXPECT_EQ(launchPauseTicksAfterBeginEpoch, NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + + nostromo.advanceAndEndTick(1000); + EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, launchPauseTicksAfterBeginEpoch - 1); + + nostromo.setNow(2026, 1, 9, 9, 8, 10); + nostromo.advanceAndEndTick(0); + auction = nostromo.getAuction(createOutput.auctionIndex).auction; + ASSERT_EQ(auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(auction.core.sellerDecisionDeadline, originalSellerDecisionDeadline); + + nostromo.advanceTicks(launchPauseTicksAfterBeginEpoch - 2); + auction = nostromo.getAuction(createOutput.auctionIndex).auction; + ASSERT_EQ(auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, 0U); + EXPECT_GT(auction.core.sellerDecisionDeadline, originalSellerDecisionDeadline); + + auto shiftedDeadline = auction.core.sellerDecisionDeadline; + shiftedDeadline.add(0, 0, 0, 0, 0, -1); + nostromo.setNow(shiftedDeadline.getYear(), shiftedDeadline.getMonth(), shiftedDeadline.getDay(), shiftedDeadline.getHour(), + shiftedDeadline.getMinute(), shiftedDeadline.getSecond()); + nostromo.advanceAndEndTick(0); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); + + shiftedDeadline = auction.core.sellerDecisionDeadline; + shiftedDeadline.add(0, 0, 0, 0, 0, 1); + nostromo.setNow(shiftedDeadline.getYear(), shiftedDeadline.getMonth(), shiftedDeadline.getDay(), shiftedDeadline.getHour(), + shiftedDeadline.getMinute(), shiftedDeadline.getSecond()); + nostromo.advanceAndEndTick(0); + auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); +} + +TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) +{ + const uint8 routeModes[] = {0, 1}; + for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) + { + SCOPED_TRACE(::testing::Message() << "routeAllFeesToDevelopment=" << static_cast(routeModes[routeIndex])); + ContractTestingNOST nostromo; + const uint8 routeMode = routeModes[routeIndex]; + const id seller(221 + routeIndex, 222 + routeIndex, 223 + routeIndex, 224 + routeIndex); + const id bidder(225 + routeIndex, 226 + routeIndex, 227 + routeIndex, 228 + routeIndex); + const uint64 assetName = assetNameFromString(routeMode ? "STDENR1" : "STDENR0"); + const Asset asset{seller, assetName}; + + nostromo.setRouteAllFeesToDevelopment(routeMode); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + // Isolate the sale-fee pool from the auction creation fee. + nostromo.endEpoch(); + constexpr uint64 expectedSellerPayout = 935000ULL; + constexpr uint64 expectedShareholderDividend = 45000ULL; + constexpr uint64 expectedManagementFee = 5000ULL; + constexpr uint64 expectedDevelopmentFee = 5000ULL; + constexpr uint64 expectedCoordinatorFee = 10000ULL; + constexpr uint64 expectedTotalFees = 65000ULL; + const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedShareholderDividend); + const sint64 sellerBalanceBefore = getBalance(seller); + const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, + NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.core.allocatedQuantity, 1ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); + EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, expectedSellerPayout); + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedTotalFees); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, expectedTotalFees); + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 0ULL); + + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedTotalFees); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); } - } - void getStatsChecker(uint64 epochRevenu_t, uint64 totalPoolWeight_t, uint32 numberOfCreatedProject_t, uint32 numberOfFundraising_t, uint32 numberOfRegister_t) - { - EXPECT_EQ(epochRevenu_t, epochRevenue); - EXPECT_EQ(totalPoolWeight_t, totalPoolWeight); - EXPECT_EQ(numberOfCreatedProject_t, numberOfCreatedProject); - EXPECT_EQ(numberOfFundraising_t, numberOfFundraising); - EXPECT_EQ(numberOfRegister_t, numberOfRegister); - } - void removeElementAfterClaimChecker(id user) - { - uint32 tp; - EXPECT_EQ(investors.get(user, tmpInvestedList), 0); - EXPECT_EQ(numberOfInvestedProjects.get(user, tp), 0); - } -}; + else + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedManagementFee); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedDevelopmentFee); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedCoordinatorFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); + } + } +} -class ContractTestingNostromo : protected ContractTesting +TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction) { -public: - ContractTestingNostromo() - { - initEmptySpectrum(); - initEmptyUniverse(); - INIT_CONTRACT(NOST); - callSystemProcedure(NOST_CONTRACT_INDEX, INITIALIZE); - INIT_CONTRACT(QX); - callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); - INIT_CONTRACT(QUOTTERY); - callSystemProcedure(QUOTTERY_CONTRACT_INDEX, INITIALIZE); - } - NostromoChecker* getState() - { - return (NostromoChecker*)contractStates[NOST_CONTRACT_INDEX]; - } - void endEpoch(bool expectSuccess = true) - { - callSystemProcedure(NOST_CONTRACT_INDEX, END_EPOCH, expectSuccess); - } - void registerInTier(const id& registerId, - uint32 tierLevel, - uint64 depositeAmount) - { - NOST::registerInTier_input input; - NOST::registerInTier_output output; - - input.tierLevel = tierLevel; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 1, input, output, registerId, depositeAmount); - } - void logoutFromTier(const id& registerId) - { - NOST::logoutFromTier_input input; - NOST::logoutFromTier_output output; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 2, input, output, registerId, 0); - } - void createProject(const id& registerId, - uint64 tokenName, - uint64 supply, - uint32 startYear, - uint32 startMonth, - uint32 startDay, - uint32 startHour, - uint32 endYear, - uint32 endMonth, - uint32 endDay, - uint32 endHour) - { - NOST::createProject_input input; - NOST::createProject_output output; - - input.tokenName = tokenName; - input.supply = supply; - input.startYear = startYear; - input.startMonth = startMonth; - input.startDay = startDay; - input.startHour = startHour; - input.endYear = endYear; - input.endMonth = endMonth; - input.endDay = endDay; - input.endHour = endHour; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 3, input, output, registerId, NOSTROMO_CREATE_PROJECT_FEE); - } - void voteInProject(const id& registerId, - uint32 indexOfProject, - bit decision) - { - NOST::voteInProject_input input; - NOST::voteInProject_output output; - - input.decision = decision; - input.indexOfProject = indexOfProject; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 4, input, output, registerId, 0); - } - void createFundraising(const id& registerId, - uint64 tokenPrice, - uint64 soldAmount, - uint64 requiredFunds, - - uint32 indexOfProject, - uint32 firstPhaseStartYear, - uint32 firstPhaseStartMonth, - uint32 firstPhaseStartDay, - uint32 firstPhaseStartHour, - uint32 firstPhaseEndYear, - uint32 firstPhaseEndMonth, - uint32 firstPhaseEndDay, - uint32 firstPhaseEndHour, - - uint32 secondPhaseStartYear, - uint32 secondPhaseStartMonth, - uint32 secondPhaseStartDay, - uint32 secondPhaseStartHour, - uint32 secondPhaseEndYear, - uint32 secondPhaseEndMonth, - uint32 secondPhaseEndDay, - uint32 secondPhaseEndHour, - - uint32 thirdPhaseStartYear, - uint32 thirdPhaseStartMonth, - uint32 thirdPhaseStartDay, - uint32 thirdPhaseStartHour, - uint32 thirdPhaseEndYear, - uint32 thirdPhaseEndMonth, - uint32 thirdPhaseEndDay, - uint32 thirdPhaseEndHour, - - uint32 listingStartYear, - uint32 listingStartMonth, - uint32 listingStartDay, - uint32 listingStartHour, - - uint32 cliffEndYear, - uint32 cliffEndMonth, - uint32 cliffEndDay, - uint32 cliffEndHour, - - uint32 vestingEndYear, - uint32 vestingEndMonth, - uint32 vestingEndDay, - uint32 vestingEndHour, - - uint8 threshold, - uint8 TGE, - uint8 stepOfVesting) - { - NOST::createFundraising_input input; - NOST::createFundraising_output output; - - input.tokenPrice = tokenPrice; - input.soldAmount = soldAmount; - input.requiredFunds = requiredFunds; - - input.indexOfProject = indexOfProject; - input.firstPhaseStartYear = firstPhaseStartYear; - input.firstPhaseStartMonth = firstPhaseStartMonth; - input.firstPhaseStartDay = firstPhaseStartDay; - input.firstPhaseStartHour = firstPhaseStartHour; - input.firstPhaseEndYear = firstPhaseEndYear; - input.firstPhaseEndMonth = firstPhaseEndMonth; - input.firstPhaseEndDay = firstPhaseEndDay; - input.firstPhaseEndHour = firstPhaseEndHour; - - input.secondPhaseStartYear = secondPhaseStartYear; - input.secondPhaseStartMonth = secondPhaseStartMonth; - input.secondPhaseStartDay = secondPhaseStartDay; - input.secondPhaseStartHour = secondPhaseStartHour; - input.secondPhaseEndYear = secondPhaseEndYear; - input.secondPhaseEndMonth = secondPhaseEndMonth; - input.secondPhaseEndDay = secondPhaseEndDay; - input.secondPhaseEndHour = secondPhaseEndHour; - - input.thirdPhaseStartYear = thirdPhaseStartYear; - input.thirdPhaseStartMonth = thirdPhaseStartMonth; - input.thirdPhaseStartDay = thirdPhaseStartDay; - input.thirdPhaseStartHour = thirdPhaseStartHour; - input.thirdPhaseEndYear = thirdPhaseEndYear; - input.thirdPhaseEndMonth = thirdPhaseEndMonth; - input.thirdPhaseEndDay = thirdPhaseEndDay; - input.thirdPhaseEndHour = thirdPhaseEndHour; - - input.listingStartYear = listingStartYear; - input.listingStartMonth = listingStartMonth; - input.listingStartDay = listingStartDay; - input.listingStartHour = listingStartHour; - - input.cliffEndYear = cliffEndYear; - input.cliffEndMonth = cliffEndMonth; - input.cliffEndDay = cliffEndDay; - input.cliffEndHour = cliffEndHour; - - input.vestingEndYear = vestingEndYear; - input.vestingEndMonth = vestingEndMonth; - input.vestingEndDay = vestingEndDay; - input.vestingEndHour = vestingEndHour; - - input.threshold = threshold; - input.TGE = TGE; - input.stepOfVesting = stepOfVesting; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 5, input, output, registerId, NOSTROMO_QX_TOKEN_ISSUANCE_FEE); - } - void investInProject(const id& investorId, - uint32 indexOfFundraising, - uint64 investmentAmount) - { - NOST::investInProject_input input; - NOST::investInProject_output output; - - input.indexOfFundraising = indexOfFundraising; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 6, input, output, investorId, investmentAmount); - } - uint64 claimToken(const id& claimerId, - uint64 claimAmount, - uint32 indexOfFundraising) - { - NOST::claimToken_input input; - NOST::claimToken_output output; - - input.amount = claimAmount; - input.indexOfFundraising = indexOfFundraising; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 7, input, output, claimerId, 0); - return output.claimedAmount; - } - void upgradeTier(const id& registerId, - uint32 newTierLevel, - uint64 depositAmount) - { - NOST::upgradeTier_input input; - NOST::upgradeTier_output output; - - input.newTierLevel = newTierLevel; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 8, input, output, registerId, depositAmount); - } - sint64 TransferShareManagementRights(const id& user, Asset asset, sint64 numberOfShares, uint32 newManagingContractIndex) - { - NOST::TransferShareManagementRights_input input; - NOST::TransferShareManagementRights_output output; - - input.asset = asset; - input.newManagingContractIndex = newManagingContractIndex; - input.numberOfShares = numberOfShares; - - invokeUserProcedure(NOST_CONTRACT_INDEX, 9, input, output, user, 100); - - return output.transferredNumberOfShares; - } - NOST::getStats_output getStats() const - { - NOST::getStats_input input; - NOST::getStats_output output; - - callFunction(NOST_CONTRACT_INDEX, 1, input, output); - return output; - } - NOST::getTierLevelByUser_output getTierLevelByUser(const id& registerId) const - { - NOST::getTierLevelByUser_input input; - NOST::getTierLevelByUser_output output; - - input.userId = registerId; - callFunction(NOST_CONTRACT_INDEX, 2, input, output); - return output; - } - NOST::getUserVoteStatus_output getUserVoteStatus(const id& registerId) const - { - NOST::getUserVoteStatus_input input; - NOST::getUserVoteStatus_output output; - - input.userId = registerId; - callFunction(NOST_CONTRACT_INDEX, 3, input, output); - return output; - } - NOST::checkTokenCreatability_output checkTokenCreatability(uint64 tokenName) const - { - NOST::checkTokenCreatability_input input; - NOST::checkTokenCreatability_output output; - - input.tokenName = tokenName; - callFunction(NOST_CONTRACT_INDEX, 4, input, output); - return output; - } - NOST::getNumberOfInvestedProjects_output getNumberOfInvestedProjects(const id& invsetorId) const - { - NOST::getNumberOfInvestedProjects_input input; - NOST::getNumberOfInvestedProjects_output output; - - input.userId = invsetorId; - callFunction(NOST_CONTRACT_INDEX, 5, input, output); - return output; - } - NOST::getProjectByIndex_output getProjectByIndex(uint32 indexOfProject) const - { - NOST::getProjectByIndex_input input; - NOST::getProjectByIndex_output output; - - input.indexOfProject = indexOfProject; - callFunction(NOST_CONTRACT_INDEX, 6, input, output); - return output; - } - NOST::getFundarasingByIndex_output getFundarasingByIndex(uint32 indexOfFundraising) const - { - NOST::getFundarasingByIndex_input input; - NOST::getFundarasingByIndex_output output; - - input.indexOfFundarasing = indexOfFundraising; - callFunction(NOST_CONTRACT_INDEX, 7, input, output); - return output; - } - NOST::getProjectIndexListByCreator_output getProjectIndexListByCreator(const id& creatorId) const - { - NOST::getProjectIndexListByCreator_input input; - NOST::getProjectIndexListByCreator_output output; - - input.creator = creatorId; - callFunction(NOST_CONTRACT_INDEX, 8, input, output); - return output; - } - NOST::getInfoUserInvested_output getInfoUserInvested(const id& investorId) const - { - NOST::getInfoUserInvested_input input; - NOST::getInfoUserInvested_output output; - - input.investorId = investorId; - callFunction(NOST_CONTRACT_INDEX, 9, input, output); - return output; - } - uint64 getMaxClaimAmount(const id& investorId, uint32 indexOfFundraising) const - { - NOST::getMaxClaimAmount_input input; - NOST::getMaxClaimAmount_output output; - - input.investorId = investorId; - input.indexOfFundraising = indexOfFundraising; - callFunction(NOST_CONTRACT_INDEX, 10, input, output); - return output.amount; - } -}; + { + ContractTestingNOST nostromo; + const id seller(231, 232, 233, 234); + const id bidder(235, 236, 237, 238); + const uint64 assetName = assetNameFromString("PENACC"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = nostromo.createAuction( + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, + NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 200000ULL, NOST_STANDARD_MIN_PRICE + 200000ULL) + .errorCode, + NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); + + const auto forbidden = nostromo.resolvePendingStandardAuction(id(999, 999, 999, 999), createOutput.auctionIndex, true); + EXPECT_EQ(forbidden.errorCode, NOST::EAuctionError::Forbidden); + + const auto acceptOutput = nostromo.resolvePendingStandardAuction(seller, createOutput.auctionIndex, true); + EXPECT_EQ(acceptOutput.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); + } + + { + ContractTestingNOST nostromo; + const id seller(239, 240, 241, 242); + const id bidder(243, 244, 245, 246); + const uint64 assetName = assetNameFromString("PENREJ"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = nostromo.createAuction( + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, + NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + nostromo.seedUser(bidder, NOST_STANDARD_MIN_PRICE + 300000ULL); + const sint64 bidderBeforeBid = getBalance(bidder); + ASSERT_EQ(nostromo + .placeBidWithFundedReward(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 200000ULL, + NOST_STANDARD_MIN_PRICE + 200000ULL) + .errorCode, + NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + const auto rejectOutput = nostromo.resolvePendingStandardAuction(seller, createOutput.auctionIndex, false); + EXPECT_EQ(rejectOutput.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(rejectOutput.refundedAmount, NOST_STANDARD_MIN_PRICE + 200000ULL); + + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + const auto participant = nostromo.getParticipant(createOutput.auctionIndex, bidder); + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.core.allocatedQuantity, 0ULL); + EXPECT_TRUE(isZero(auction.core.highestBidder)); + ASSERT_EQ(participant.found, 1); + EXPECT_EQ(participant.participantData.allocatedQuantity, 0ULL); + EXPECT_EQ(participant.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(nostromo.managedShares(asset, seller), 1); + EXPECT_EQ(getBalance(bidder), bidderBeforeBid); + } + + { + ContractTestingNOST nostromo; + const id seller(247, 248, 249, 250); + const id bidder(251, 252, 253, 254); + const uint64 assetName = assetNameFromString("PENTMO"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = nostromo.createAuction( + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, + NOST_STANDARD_MIN_PRICE + 500000ULL, NOST_STANDARD_MIN_BID_INCREMENT)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 200000ULL, NOST_STANDARD_MIN_PRICE + 200000ULL) + .errorCode, + NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); + + nostromo.advanceAndEndTick((NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS + 1ULL) * 1000ULL); + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; + const auto participant = nostromo.getParticipant(createOutput.auctionIndex, bidder); + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.core.allocatedQuantity, 1ULL); + ASSERT_EQ(participant.found, 1); + EXPECT_EQ(participant.participantData.allocatedQuantity, 1ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); + } +} + +TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) +{ + const uint8 routeModes[] = {0, 1}; + for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) + { + SCOPED_TRACE(::testing::Message() << "routeAllFeesToDevelopment=" << static_cast(routeModes[routeIndex])); + { + ContractTestingNOST nostromo; + const uint8 routeMode = routeModes[routeIndex]; + const id seller(261 + routeIndex, 262 + routeIndex, 263 + routeIndex, 264 + routeIndex); + const uint64 assetName = assetNameFromString(routeMode ? "CANBT1" : "CANBT0"); + const Asset asset{seller, assetName}; + + nostromo.setRouteAllFeesToDevelopment(routeMode); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 10), 10); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 1000)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + nostromo.endEpoch(); + const sint64 sellerBefore = getBalance(seller); + const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + constexpr uint64 expectedShareholderDividend = 727ULL; + constexpr uint64 expectedRecipientFee = 91ULL; + const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedShareholderDividend); + + const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionIndex, 1000); + EXPECT_EQ(cancelOutput.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(cancelOutput.refundedAmount, 0ULL); + EXPECT_EQ(cancelOutput.cancellationFee, 1000ULL); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Cancelled); + EXPECT_EQ(nostromo.managedShares(asset, seller), 10); + EXPECT_EQ(getBalance(seller) - sellerBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 1000ULL); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 1000ULL); + nostromo.endEpoch(); + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 1000ULL); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); + } + else + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedRecipientFee); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedRecipientFee); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedRecipientFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); + } + } + + { + ContractTestingNOST nostromo; + const uint8 routeMode = routeModes[routeIndex]; + const id seller(273 + routeIndex, 274 + routeIndex, 275 + routeIndex, 276 + routeIndex); + const uint64 assetName = assetNameFromString(routeMode ? "CANST1" : "CANST0"); + const Asset asset{seller, assetName}; + + nostromo.setRouteAllFeesToDevelopment(routeMode); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + nostromo.endEpoch(); + const sint64 sellerBefore = getBalance(seller); + const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + constexpr uint64 expectedShareholderDividend = 72700ULL; + constexpr uint64 expectedRecipientFee = 9100ULL; + const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedShareholderDividend); + + const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionIndex, 100000); + EXPECT_EQ(cancelOutput.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(cancelOutput.refundedAmount, 0ULL); + EXPECT_EQ(cancelOutput.cancellationFee, 100000ULL); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Cancelled); + EXPECT_EQ(nostromo.managedShares(asset, seller), 1); + EXPECT_EQ(getBalance(seller) - sellerBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 100000ULL); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 100000ULL); + nostromo.endEpoch(); + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 100000ULL); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); + } + else + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedRecipientFee); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedRecipientFee); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedRecipientFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); + } + } + } +} + +TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeRemainderAuction) +{ + const uint8 routeModes[] = {0, 1}; + for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) + { + SCOPED_TRACE(::testing::Message() << "routeAllFeesToDevelopment=" << static_cast(routeModes[routeIndex])); + { + ContractTestingNOST nostromo; + const uint8 routeMode = routeModes[routeIndex]; + const id batchSeller(277 + routeIndex, 278 + routeIndex, 279 + routeIndex, 280 + routeIndex); + const uint64 batchAssetName = assetNameFromString(routeMode ? "CANRN1" : "CANRN0"); + const Asset batchAsset{batchSeller, batchAssetName}; + + nostromo.setRouteAllFeesToDevelopment(routeMode); + EXPECT_EQ(nostromo.issueAsset(batchSeller, batchAssetName, 7), 7); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(batchSeller, batchAsset, 7), 7); + + const auto batchCreateOutput = nostromo.createAuction(batchSeller, ContractTestingNOST::makeBatchAuctionInput(batchAsset, 7, 333)); + ASSERT_EQ(batchCreateOutput.errorCode, NOST::EAuctionError::Success); + nostromo.endEpoch(); + + constexpr uint64 expectedBatchShareholderDividend = 170ULL; + constexpr uint64 expectedBatchRecipientFee = 21ULL; + const sint64 expectedBatchDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedBatchShareholderDividend); + const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + + const auto batchCancelOutput = nostromo.cancelAuction(batchSeller, batchCreateOutput.auctionIndex, 233); + EXPECT_EQ(batchCancelOutput.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(batchCancelOutput.cancellationFee, 233ULL); + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 233ULL); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 233ULL); + nostromo.endEpoch(); + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 233ULL); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); + } + else + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedBatchRecipientFee); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedBatchRecipientFee); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedBatchRecipientFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedBatchDividendPoolIncrease); + } + EXPECT_EQ(expectedBatchShareholderDividend + expectedBatchRecipientFee * 3ULL, batchCancelOutput.cancellationFee); + } + + { + ContractTestingNOST smallFeeNostromo; + const uint8 routeMode = routeModes[routeIndex]; + const id standardSeller(281 + routeIndex, 282 + routeIndex, 283 + routeIndex, 284 + routeIndex); + const uint64 standardAssetName = assetNameFromString(routeMode ? "CANON1" : "CANON0"); + const Asset standardAsset{standardSeller, standardAssetName}; + + smallFeeNostromo.setRouteAllFeesToDevelopment(routeMode); + EXPECT_EQ(smallFeeNostromo.issueAsset(standardSeller, standardAssetName, 1), 1); + EXPECT_EQ(smallFeeNostromo.transferShareManagementRightsToNostromo(standardSeller, standardAsset, 1), 1); + + const auto standardCreateOutput = + smallFeeNostromo.createAuction(standardSeller, ContractTestingNOST::makeBatchAuctionInput(standardAsset, 1, 19)); + ASSERT_EQ(standardCreateOutput.errorCode, NOST::EAuctionError::Success); + smallFeeNostromo.endEpoch(); + + const sint64 expectedSmallDividendPoolIncrease = smallFeeNostromo.expectedDividendPoolIncrease(1ULL); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + + const auto standardCancelOutput = smallFeeNostromo.cancelAuction(standardSeller, standardCreateOutput.auctionIndex, 1); + EXPECT_EQ(standardCancelOutput.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(standardCancelOutput.cancellationFee, 1ULL); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 1ULL); + EXPECT_EQ(smallFeeNostromo.getNostromoFeePool().totalAmount, 1ULL); + smallFeeNostromo.endEpoch(); + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 1ULL); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); + } + else + { + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedSmallDividendPoolIncrease); + } + } + } +} + +TEST(ContractNostromoAuction, CancelAuctionRejectsAfterAcceptedBidWithoutSideEffectsAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(281, 282, 283, 284); + const id bidder(285, 286, 287, 288); + const uint64 assetName = assetNameFromString("CANINV"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, 1, 12).errorCode, NOST::EAuctionError::Success); + + const auto notFound = nostromo.cancelAuction(seller, 800, 10); + EXPECT_EQ(notFound.errorCode, NOST::EAuctionError::AuctionNotFound); + + const auto forbidden = nostromo.cancelAuction(bidder, createOutput.auctionIndex, 10); + EXPECT_EQ(forbidden.errorCode, NOST::EAuctionError::Forbidden); + + const auto bidderBalanceBeforeCancel = getBalance(bidder); + const auto participantBeforeCancel = nostromo.getParticipant(createOutput.auctionIndex, bidder); + const auto rejected = nostromo.cancelAuction(seller, createOutput.auctionIndex, 0); + EXPECT_EQ(rejected.errorCode, NOST::EAuctionError::AuctionHasAcceptedBid); + EXPECT_EQ(rejected.refundedAmount, 0ULL); + EXPECT_EQ(getBalance(bidder), bidderBalanceBeforeCancel); + const auto participantAfterCancel = nostromo.getParticipant(createOutput.auctionIndex, bidder); + ASSERT_EQ(participantBeforeCancel.found, 1); + ASSERT_EQ(participantAfterCancel.found, 1); + EXPECT_EQ(participantAfterCancel.participantData.escrowedAmount, participantBeforeCancel.participantData.escrowedAmount); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); + EXPECT_EQ(nostromo.managedShares(asset, NOST_CONTRACT_ID), 2); + } + + { + ContractTestingNOST standardNostromo; + const id standardSeller(1281, 1282, 1283, 1284); + const id standardBidder(1285, 1286, 1287, 1288); + const Asset standardAsset{standardSeller, assetNameFromString("CANSTD")}; + ASSERT_EQ(standardNostromo.issueAsset(standardSeller, standardAsset.assetName, 1), 1); + ASSERT_EQ(standardNostromo.transferShareManagementRightsToNostromo(standardSeller, standardAsset, 1), 1); + const auto standardCreate = standardNostromo.createAuction( + standardSeller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(standardAsset, 1))); + ASSERT_EQ(standardCreate.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ( + standardNostromo.placeBid(standardBidder, standardCreate.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, + NOST::EAuctionError::Success); + EXPECT_EQ(standardNostromo.cancelAuction(standardSeller, standardCreate.auctionIndex, 0).errorCode, + NOST::EAuctionError::AuctionHasAcceptedBid); + EXPECT_EQ(standardNostromo.getAuction(standardCreate.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); + } +} + +TEST(ContractNostromoAuction, CancelAuctionRejectsInvalidCasesWithoutBidsAuction) +{ + ContractTestingNOST nostromo; + const id seller(289, 290, 291, 292); + const id outsider(293, 294, 295, 296); + const uint64 assetName = assetNameFromString("CANIN2"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const auto notFound = nostromo.cancelAuction(seller, 801, 10); + EXPECT_EQ(notFound.errorCode, NOST::EAuctionError::AuctionNotFound); + + const auto forbidden = nostromo.cancelAuction(outsider, createOutput.auctionIndex, 10); + EXPECT_EQ(forbidden.errorCode, NOST::EAuctionError::Forbidden); + + const auto insufficient = nostromo.cancelAuction(seller, createOutput.auctionIndex, 1); + EXPECT_EQ(insufficient.errorCode, NOST::EAuctionError::InsufficientFunds); + + const auto success = nostromo.cancelAuction(seller, createOutput.auctionIndex, 2); + EXPECT_EQ(success.errorCode, NOST::EAuctionError::Success); + + const auto closed = nostromo.cancelAuction(seller, createOutput.auctionIndex, 2); + EXPECT_EQ(closed.errorCode, NOST::EAuctionError::AuctionClosed); +} + +TEST(ContractNostromoAuction, FinalizationArchivesRecordsAndReusesActiveSlotsAuction) +{ + ContractTestingNOST nostromo; + const id seller(481, 482, 483, 484); + const id bidderA(485, 486, 487, 488); + const id bidderB(489, 490, 491, 492); + const Asset asset{seller, assetNameFromString("REUSEA")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); + auto firstInput = + ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE, + NOST_STANDARD_MIN_BID_INCREMENT, NOST_STANDARD_MIN_PRICE); + const auto firstAuction = nostromo.createAuction(seller, firstInput); + ASSERT_EQ(firstAuction.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidderA, firstAuction.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, + NOST::EAuctionError::Success); + + EXPECT_EQ(nostromo.stateData().auctionList.population(), 0ULL); + EXPECT_EQ(nostromo.stateData().participantHistoryCounter, 1ULL); + EXPECT_EQ(nostromo.getAuction(firstAuction.auctionIndex).auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.getParticipant(firstAuction.auctionIndex, bidderA).found, 1); + + const auto secondAuction = nostromo.createAuction(seller, firstInput); + ASSERT_EQ(secondAuction.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidderB, secondAuction.auctionIndex, 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE).errorCode, + NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.stateData().participantHistoryCounter, 2ULL); + EXPECT_EQ(nostromo.getAuction(secondAuction.auctionIndex).auction.core.status, NOST::EAuctionStatus::Finalized); +} + +TEST(ContractNostromoAuction, ClosedAuctionHistoryRecordsFinalizedAndCancelledAuctionsAuction) +{ + ContractTestingNOST nostromo; + const id finalizedSeller(501, 502, 503, 504); + const id bidder(505, 506, 507, 508); + const uint64 finalizedAssetName = assetNameFromString("HISFIN"); + const Asset finalizedAsset{finalizedSeller, finalizedAssetName}; + const id cancelledSeller(509, 510, 511, 512); + const uint64 cancelledAssetName = assetNameFromString("HISCAN"); + const Asset cancelledAsset{cancelledSeller, cancelledAssetName}; + + EXPECT_EQ(nostromo.issueAsset(finalizedSeller, finalizedAssetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(finalizedSeller, finalizedAsset, 1), 1); + const auto finalizedCreateOutput = nostromo.createAuction(finalizedSeller, ContractTestingNOST::makeBatchAuctionInput(finalizedAsset, 1, 10)); + ASSERT_EQ(finalizedCreateOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, finalizedCreateOutput.auctionIndex, 1, 10).errorCode, NOST::EAuctionError::Success); + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + EXPECT_EQ(nostromo.issueAsset(cancelledSeller, cancelledAssetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(cancelledSeller, cancelledAsset, 1), 1); + const auto cancelledCreateOutput = nostromo.createAuction(cancelledSeller, ContractTestingNOST::makeBatchAuctionInput(cancelledAsset, 1, 10)); + ASSERT_EQ(cancelledCreateOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.cancelAuction(cancelledSeller, cancelledCreateOutput.auctionIndex, 1).errorCode, NOST::EAuctionError::Success); + + const auto history = nostromo.getClosedAuctionHistory(); + EXPECT_EQ(history.totalEntries, 2ULL); + EXPECT_TRUE(containsAuctionIndex(history.auctionIndices, history.totalEntries, finalizedCreateOutput.auctionIndex)); + EXPECT_TRUE(containsAuctionIndex(history.auctionIndices, history.totalEntries, cancelledCreateOutput.auctionIndex)); + EXPECT_EQ(nostromo.getAuction(finalizedCreateOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.getAuction(cancelledCreateOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Cancelled); +} + +TEST(ContractNostromoAuction, ClosedAuctionHistoryGetterExposesRingBufferOverwriteAuction) +{ + ContractTestingNOST nostromo; + const uint64 overwrittenAuctionIndex = 22000; + const uint64 latestAuctionIndex = 23000; + NOST::AuctionData archivedAuction{}; + + archivedAuction.core.auctionIndex = overwrittenAuctionIndex; + archivedAuction.core.status = NOST::EAuctionStatus::Finalized; + nostromo.stateData().closedAuctionHistory.set(0, archivedAuction); + nostromo.stateData().closedAuctionHistoryCounter = 1; + for (uint64 index = 1; index < NOST_AUCTION_HISTORY_NUM; ++index) + { + archivedAuction.core.auctionIndex = 24000 + index; + nostromo.stateData().closedAuctionHistory.set(index, archivedAuction); + ++nostromo.stateData().closedAuctionHistoryCounter; + } + archivedAuction.core.auctionIndex = latestAuctionIndex; + nostromo.stateData().closedAuctionHistory.set(0, archivedAuction); + ++nostromo.stateData().closedAuctionHistoryCounter; + + const auto history = nostromo.getClosedAuctionHistory(); + EXPECT_EQ(history.totalEntries, NOST_AUCTION_HISTORY_NUM + 1ULL); + EXPECT_FALSE(containsAuctionIndex(history.auctionIndices, history.totalEntries, overwrittenAuctionIndex)); + EXPECT_TRUE(containsAuctionIndex(history.auctionIndices, history.totalEntries, latestAuctionIndex)); + EXPECT_EQ(history.auctionIndices.get(0), latestAuctionIndex); + EXPECT_EQ(nostromo.getAuction(overwrittenAuctionIndex).found, 0); + EXPECT_EQ(nostromo.getAuction(latestAuctionIndex).found, 1); +} + +TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) +{ + ContractTestingNOST nostromo; + const id outsider(291, 292, 293, 294); + const id newManagement(295, 296, 297, 298); + + NOST::SetAuctionFees_input coordinatorInput{}; + coordinatorInput.privateAuctionFee = 60000000; + coordinatorInput.publicAuctionCreationFee = 123; + coordinatorInput.auctionCancellationFeeBasisPoints = 900; + coordinatorInput.managementFeeBasisPoints = 60; + coordinatorInput.developmentFeeBasisPoints = 70; + coordinatorInput.takeoverCoordinatorFeeBasisPoints = 80; + coordinatorInput.shareholderDividendBasisPoints = 8500; + coordinatorInput.shareholderFeeBasisPointsTier1 = 400; + coordinatorInput.shareholderFeeBasisPointsTier2 = 350; + coordinatorInput.shareholderFeeBasisPointsTier3 = 300; + coordinatorInput.shareholderFeeBasisPointsTier4 = 250; + const auto defaultFees = nostromo.getAuctionFees(); + + const auto coordinatorForbidden = nostromo.setAuctionFees(outsider, coordinatorInput); + EXPECT_EQ(coordinatorForbidden.errorCode, NOST::EAuctionError::Forbidden); + expectAuctionFeesEqual(nostromo.getAuctionFees(), defaultFees); + + NOST::SetAuctionFees_input invalidCoordinatorInput = coordinatorInput; + invalidCoordinatorInput.privateAuctionFee = -1; + const auto coordinatorInvalid = nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), invalidCoordinatorInput); + EXPECT_EQ(coordinatorInvalid.errorCode, NOST::EAuctionError::InvalidInput); + expectAuctionFeesEqual(nostromo.getAuctionFees(), defaultFees); + + const auto coordinatorSuccess = nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput); + EXPECT_EQ(coordinatorSuccess.errorCode, NOST::EAuctionError::Success); + + auto fees = nostromo.getAuctionFees(); + EXPECT_EQ(fees.privateAuctionFee, 60000000); + EXPECT_EQ(fees.auctionCancellationFeeBasisPoints, 900ULL); + EXPECT_EQ(fees.managementFeeBasisPoints, 60ULL); + EXPECT_EQ(fees.developmentFeeBasisPoints, 70ULL); + EXPECT_EQ(fees.takeoverCoordinatorFeeBasisPoints, 80ULL); + EXPECT_EQ(fees.shareholderDividendBasisPoints, 8500ULL); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier1, 400ULL); + EXPECT_EQ(fees.publicAuctionCreationFee, 123LL); + + const id managementBeforeRejectedUpdates = nostromo.getFeeRecipients().management; + const auto setManagementForbidden = nostromo.setManagement(outsider, newManagement); + EXPECT_EQ(setManagementForbidden.errorCode, NOST::EAuctionError::Forbidden); + EXPECT_EQ(nostromo.getFeeRecipients().management, managementBeforeRejectedUpdates); + + const auto setManagementInvalid = nostromo.setManagement(ContractTestingNOST::takeoverCoordinatorWallet(), NULL_ID); + EXPECT_EQ(setManagementInvalid.errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.getFeeRecipients().management, managementBeforeRejectedUpdates); + + const auto setManagementSuccess = nostromo.setManagement(ContractTestingNOST::takeoverCoordinatorWallet(), newManagement); + EXPECT_EQ(setManagementSuccess.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getFeeRecipients().management, newManagement); + + NOST::SetAuctionFeesByManagement_input managementInput{}; + managementInput.privateAuctionFee = 70000000; + managementInput.publicAuctionCreationFee = 456; + managementInput.auctionCancellationFeeBasisPoints = 800; + managementInput.managementFeeBasisPoints = 90; + managementInput.developmentFeeBasisPoints = 110; + managementInput.shareholderFeeBasisPointsTier1 = 300; + managementInput.shareholderFeeBasisPointsTier2 = 250; + managementInput.shareholderFeeBasisPointsTier3 = 200; + managementInput.shareholderFeeBasisPointsTier4 = 150; + + const auto coordinatorConfiguredFees = nostromo.getAuctionFees(); + const auto oldManagementForbidden = nostromo.setAuctionFeesByManagement(ContractTestingNOST::managementWallet(), managementInput); + EXPECT_EQ(oldManagementForbidden.errorCode, NOST::EAuctionError::Forbidden); + expectAuctionFeesEqual(nostromo.getAuctionFees(), coordinatorConfiguredFees); + + NOST::SetAuctionFeesByManagement_input invalidManagementInput = managementInput; + invalidManagementInput.managementFeeBasisPoints = 9900; + invalidManagementInput.developmentFeeBasisPoints = 200; + const auto managementInvalid = nostromo.setAuctionFeesByManagement(newManagement, invalidManagementInput); + EXPECT_EQ(managementInvalid.errorCode, NOST::EAuctionError::InvalidInput); + expectAuctionFeesEqual(nostromo.getAuctionFees(), coordinatorConfiguredFees); + + const auto managementSuccess = nostromo.setAuctionFeesByManagement(newManagement, managementInput); + EXPECT_EQ(managementSuccess.errorCode, NOST::EAuctionError::Success); + + fees = nostromo.getAuctionFees(); + EXPECT_EQ(fees.privateAuctionFee, 70000000); + EXPECT_EQ(fees.publicAuctionCreationFee, 456LL); + EXPECT_EQ(fees.auctionCancellationFeeBasisPoints, 800ULL); + EXPECT_EQ(fees.managementFeeBasisPoints, 90ULL); + EXPECT_EQ(fees.developmentFeeBasisPoints, 110ULL); + EXPECT_EQ(fees.takeoverCoordinatorFeeBasisPoints, 80ULL); + EXPECT_EQ(fees.shareholderDividendBasisPoints, 8500ULL); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier1, 300ULL); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier2, 250ULL); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier3, 200ULL); + EXPECT_EQ(fees.shareholderFeeBasisPointsTier4, 150ULL); +} + +TEST(ContractNostromoAuction, BatchSettlementAutomaticallyFlushesLargeSellerPayoutAtEndEpochAuction) +{ + ContractTestingNOST nostromo; + const id seller(901, 902, 903, 904); + const Asset asset{seller, assetNameFromString("BIGPAY")}; + constexpr uint64 bidderCount = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL + 1ULL; + auto feeInput = nostromo.makeCoordinatorFeeInput(0); + feeInput.managementFeeBasisPoints = 0; + feeInput.developmentFeeBasisPoints = 0; + feeInput.takeoverCoordinatorFeeBasisPoints = 0; + feeInput.shareholderFeeBasisPointsTier1 = 0; + feeInput.shareholderFeeBasisPointsTier2 = 0; + feeInput.shareholderFeeBasisPointsTier3 = 0; + feeInput.shareholderFeeBasisPointsTier4 = 0; + ASSERT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), feeInput).errorCode, NOST::EAuctionError::Success); + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, static_cast(bidderCount)), static_cast(bidderCount)); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, static_cast(bidderCount)), static_cast(bidderCount)); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, bidderCount, 1)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + for (uint64 bidderIndex = 0; bidderIndex < bidderCount; ++bidderIndex) + { + const id bidder(1000 + bidderIndex, 2000 + bidderIndex, 3000 + bidderIndex, 4000 + bidderIndex); + const auto bid = nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, 1, static_cast(MAX_AMOUNT)); + ASSERT_EQ(bid.errorCode, NOST::EAuctionError::Success); + } + + const uint64 grossAmount = bidderCount * static_cast(MAX_AMOUNT); + const sint64 sellerBeforeSettlement = getBalance(seller); + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const uint64 expectedImmediatePayout = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL * static_cast(MAX_AMOUNT); + ASSERT_GT(grossAmount, expectedImmediatePayout); + EXPECT_EQ(static_cast(getBalance(seller) - sellerBeforeSettlement), expectedImmediatePayout); + EXPECT_EQ(nostromo.getPendingPayout(seller).amount, grossAmount - expectedImmediatePayout); + EXPECT_EQ(nostromo.getContractStats().stats.totalPendingQuPayouts, grossAmount - expectedImmediatePayout); + + nostromo.endEpoch(); + EXPECT_EQ(static_cast(getBalance(seller) - sellerBeforeSettlement), grossAmount); + EXPECT_EQ(nostromo.getPendingPayout(seller).amount, 0ULL); + EXPECT_EQ(nostromo.getContractStats().stats.totalPendingQuPayouts, 0ULL); +} + +TEST(ContractNostromoAuction, EndEpochPendingPayoutProcessingIsBoundedAuction) +{ + ContractTestingNOST nostromo; + constexpr uint64 recipientCount = NOST_END_EPOCH_PAYOUT_RECIPIENT_NUM + 1ULL; + + nostromo.seedUser(NOST_CONTRACT_ID, static_cast(recipientCount)); + for (uint64 recipientIndex = 0; recipientIndex < recipientCount; ++recipientIndex) + { + const id recipient(12000 + recipientIndex, 13000 + recipientIndex, 14000 + recipientIndex, 15000 + recipientIndex); + nostromo.ensureUser(recipient); + ASSERT_NE(nostromo.stateData().pendingQuPayouts.set(recipient, 1ULL), NULL_INDEX); + nostromo.stateData().totalPendingQuPayouts = sadd(nostromo.stateData().totalPendingQuPayouts, 1ULL); + } + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.stateData().pendingQuPayouts.population(), 1ULL); + EXPECT_EQ(nostromo.getContractStats().stats.totalPendingQuPayouts, 1ULL); + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.stateData().pendingQuPayouts.population(), 0ULL); + EXPECT_EQ(nostromo.getContractStats().stats.totalPendingQuPayouts, 0ULL); +} + +TEST(ContractNostromoAuction, EndEpochDoesNotRematerializeServiceFeesWhenPayoutQueueIsFullAuction) +{ + ContractTestingNOST nostromo; + const id seller(1601, 1602, 1603, 1604); + const id allowedBidder(1605, 1606, 1607, 1608); + const Asset asset{seller, assetNameFromString("FULQUE")}; + + nostromo.setRouteAllFeesToDevelopment(0); + ASSERT_EQ(nostromo.getRouteAllFeesToDevelopment(), 0); + ASSERT_EQ(nostromo.issueAsset(seller, asset.assetName, 1), 1); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); + ASSERT_EQ(nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, NOST::EAuctionError::Success); + + constexpr uint64 blockedPayoutAmount = static_cast(MAX_AMOUNT); + for (uint64 recipientIndex = 0; recipientIndex < NOST_PENDING_PAYOUT_NUM; ++recipientIndex) + { + const id recipient(20000 + recipientIndex, 30000 + recipientIndex, 40000 + recipientIndex, 50000 + recipientIndex); + ASSERT_NE(nostromo.stateData().pendingQuPayouts.set(recipient, blockedPayoutAmount), NULL_INDEX); + nostromo.stateData().totalPendingQuPayouts = sadd(nostromo.stateData().totalPendingQuPayouts, blockedPayoutAmount); + } + ASSERT_EQ(nostromo.stateData().pendingQuPayouts.population(), NOST_PENDING_PAYOUT_NUM); + + const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); + nostromo.endEpoch(); + + const auto poolAfterFirstEpoch = nostromo.getNostromoFeePool(); + EXPECT_EQ(poolAfterFirstEpoch.feePool.commonServiceFeeAmount, 0ULL); + EXPECT_EQ(poolAfterFirstEpoch.feePool.shareholderDividendAmount, 0ULL); + EXPECT_EQ(poolAfterFirstEpoch.feePool.managementAmount, 4550000ULL); + EXPECT_EQ(poolAfterFirstEpoch.feePool.developmentAmount, 4550000ULL); + EXPECT_EQ(poolAfterFirstEpoch.feePool.takeoverCoordinatorAmount, 4550000ULL); + EXPECT_EQ(poolAfterFirstEpoch.totalAmount, 13650000ULL); + EXPECT_EQ(nostromo.stateData().auctionShareholderDividendPool, 128ULL); + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()), managementBefore); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()), developmentBefore); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()), coordinatorBefore); + + nostromo.endEpoch(); + + const auto poolAfterRetry = nostromo.getNostromoFeePool(); + EXPECT_EQ(poolAfterRetry.feePool.commonServiceFeeAmount, 0ULL); + EXPECT_EQ(poolAfterRetry.feePool.managementAmount, poolAfterFirstEpoch.feePool.managementAmount); + EXPECT_EQ(poolAfterRetry.feePool.developmentAmount, poolAfterFirstEpoch.feePool.developmentAmount); + EXPECT_EQ(poolAfterRetry.feePool.takeoverCoordinatorAmount, poolAfterFirstEpoch.feePool.takeoverCoordinatorAmount); + EXPECT_EQ(poolAfterRetry.totalAmount, poolAfterFirstEpoch.totalAmount); + EXPECT_EQ(nostromo.stateData().auctionShareholderDividendPool, 128ULL); + EXPECT_EQ(nostromo.stateData().pendingQuPayouts.population(), NOST_PENDING_PAYOUT_NUM); +} + +TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) +{ + struct TierCase + { + uint64 grossAmount; + uint64 sellerPayout; + uint64 shareholderDividend; + uint64 managementFee; + uint64 developmentFee; + uint64 coordinatorFee; + uint64 totalFee; + uint64 assetName; + }; + + const TierCase cases[] = { + {5000000000ULL, 4675000000ULL, 225000000ULL, 25000000ULL, 25000000ULL, 50000000ULL, 325000000ULL, assetNameFromString("TIERA1")}, + {5000000001ULL, 4700000001ULL, 202500000ULL, 25000000ULL, 25000000ULL, 47500000ULL, 300000000ULL, assetNameFromString("TIERA2")}, + {50000000001ULL, 47250000001ULL, 1800000000ULL, 250000000ULL, 250000000ULL, 450000000ULL, 2750000000ULL, assetNameFromString("TIERA3")}, + {200000000001ULL, 190000000001ULL, 6300000000ULL, 1000000000ULL, 1000000000ULL, 1700000000ULL, 10000000000ULL, assetNameFromString("TIERA4")}, + }; + + const uint8 routeModes[] = {0, 1}; + for (uint64 caseIndex = 0; caseIndex < sizeof(cases) / sizeof(cases[0]); ++caseIndex) + { + for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) + { + SCOPED_TRACE(::testing::Message() << "caseIndex=" << caseIndex + << ", routeAllFeesToDevelopment=" << static_cast(routeModes[routeIndex])); + ContractTestingNOST nostromo; + const uint8 routeMode = routeModes[routeIndex]; + const id seller(301 + caseIndex * 2 + routeIndex, 302 + caseIndex * 2 + routeIndex, 303 + caseIndex * 2 + routeIndex, + 304 + caseIndex * 2 + routeIndex); + const id bidder(401 + caseIndex * 2 + routeIndex, 402 + caseIndex * 2 + routeIndex, 403 + caseIndex * 2 + routeIndex, + 404 + caseIndex * 2 + routeIndex); + const Asset asset{seller, cases[caseIndex].assetName}; + nostromo.setRouteAllFeesToDevelopment(routeMode); + EXPECT_EQ(nostromo.issueAsset(seller, cases[caseIndex].assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = nostromo.createAuction( + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), cases[caseIndex].grossAmount, + cases[caseIndex].grossAmount, NOST_STANDARD_MIN_BID_INCREMENT)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + nostromo.endEpoch(); + const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(cases[caseIndex].shareholderDividend); + const sint64 sellerBefore = getBalance(seller); + const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, cases[caseIndex].grossAmount, cases[caseIndex].grossAmount).errorCode, + NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + EXPECT_EQ(getBalance(seller) - sellerBefore, cases[caseIndex].sellerPayout); + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, cases[caseIndex].totalFee); + + const auto pendingFeePool = nostromo.getNostromoFeePool(); + EXPECT_EQ(pendingFeePool.totalAmount, cases[caseIndex].totalFee); + if (routeMode == 0) + { + const uint64 tierAmounts[] = { + pendingFeePool.feePool.shareholderDividendTier1Amount, pendingFeePool.feePool.shareholderDividendTier2Amount, + pendingFeePool.feePool.shareholderDividendTier3Amount, pendingFeePool.feePool.shareholderDividendTier4Amount}; + for (uint64 tierIndex = 0; tierIndex < sizeof(tierAmounts) / sizeof(tierAmounts[0]); ++tierIndex) + { + EXPECT_EQ(tierAmounts[tierIndex], tierIndex == caseIndex ? cases[caseIndex].shareholderDividend : 0ULL); + } + } + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 0ULL); + + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, cases[caseIndex].totalFee); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); + } + else + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, cases[caseIndex].managementFee); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, cases[caseIndex].developmentFee); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, cases[caseIndex].coordinatorFee); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); + } + } + } +} + +TEST(ContractNostromoAuction, FeeReserveGuardTriggersEmergencyPauseOnSuddenDropAuction) +{ + ContractTestingNOST nostromo; + const id seller(1001, 1002, 1003, 1004); + const id bidder(1005, 1006, 1007, 1008); + const Asset asset{seller, assetNameFromString("GRDTRG")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 5), 5); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 5), 5); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 5, 1)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + auto guardState = nostromo.getFeeReserveGuardState(); + EXPECT_EQ(guardState.isEmergencyPaused, 0); + EXPECT_EQ(guardState.dropBasisPoints, NOST_DEFAULT_FEE_RESERVE_GUARD_DROP_BP); + EXPECT_EQ(guardState.windowSeconds, NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS); + + // Drop the execution fee reserve by 20%, well past the default 10% / 10 minute guard. + const long long reserveBefore = getContractFeeReserve(NOST_CONTRACT_INDEX); + setContractFeeReserve(NOST_CONTRACT_INDEX, reserveBefore - reserveBefore / 5); + nostromo.advanceAndEndTick(1000ULL); + + guardState = nostromo.getFeeReserveGuardState(); + EXPECT_EQ(guardState.isEmergencyPaused, 1); + + const auto stats = nostromo.getContractStats(); + EXPECT_EQ(stats.stats.isEmergencyPaused, 1); + + const id newSeller(1009, 1010, 1011, 1012); + const Asset blockedAsset{newSeller, assetNameFromString("GRDBLK")}; + EXPECT_EQ(nostromo.issueAsset(newSeller, blockedAsset.assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(newSeller, blockedAsset, 2), 2); + nostromo.seedUser(newSeller, 1000); + const sint64 newSellerBefore = getBalance(newSeller); + const auto blockedCreate = nostromo.createAuctionWithFundedReward(newSeller, ContractTestingNOST::makeBatchAuctionInput(blockedAsset, 2, 1), 100); + EXPECT_EQ(blockedCreate.errorCode, NOST::EAuctionError::AuctionPaused); + EXPECT_EQ(getBalance(newSeller), newSellerBefore); + + nostromo.seedUser(bidder, 1000); + const sint64 bidderBefore = getBalance(bidder); + const auto blockedBid = nostromo.placeBidWithFundedReward(bidder, createOutput.auctionIndex, 1, 1, 100); + EXPECT_EQ(blockedBid.errorCode, NOST::EAuctionError::AuctionPaused); + EXPECT_EQ(getBalance(bidder), bidderBefore); + + const sint64 sellerBeforeCancel = getBalance(seller); + const auto blockedCancel = nostromo.cancelAuction(seller, createOutput.auctionIndex, 100); + EXPECT_EQ(blockedCancel.errorCode, NOST::EAuctionError::AuctionPaused); + // cancelAuction() seeds the reward before invoking; a paused call refunds it in full, netting the seeded amount. + EXPECT_EQ(getBalance(seller), sellerBeforeCancel + 100); + + // Non-owners cannot resume the contract. + EXPECT_EQ(nostromo.setEmergencyPause(bidder, false).errorCode, NOST::EAuctionError::Forbidden); + EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 1); + + // The coordinator can manually resume operation. + EXPECT_EQ(nostromo.setEmergencyPause(ContractTestingNOST::takeoverCoordinatorWallet(), false).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 0); + + nostromo.advanceAndEndTick(1000ULL); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); +} + +TEST(ContractNostromoAuction, FeeReserveGuardResamplesWindowWithoutFalseTriggerAuction) +{ + ContractTestingNOST nostromo; + + const long long baseline = getContractFeeReserve(NOST_CONTRACT_INDEX); + + // A gradual decline spread across multiple guard windows should never trip the single-window drop threshold. + setContractFeeReserve(NOST_CONTRACT_INDEX, baseline - baseline / 20); // -5% + nostromo.advanceAndEndTick((NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS + 10ULL) * 1000ULL); + EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 0); + + const long long afterFirstDrop = getContractFeeReserve(NOST_CONTRACT_INDEX); + setContractFeeReserve(NOST_CONTRACT_INDEX, afterFirstDrop - afterFirstDrop / 20); // another -5% + nostromo.advanceAndEndTick((NOST_DEFAULT_FEE_RESERVE_GUARD_WINDOW_SECONDS + 10ULL) * 1000ULL); + EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 0); +} + +TEST(ContractNostromoAuction, SetFeeReserveGuardConfigValidatesAndRestrictsCallerAuction) +{ + ContractTestingNOST nostromo; + const id stranger(1101, 1102, 1103, 1104); + const NOST::GetFeeReserveGuardState_output& defaultGuardState = nostromo.getFeeReserveGuardState(); + + EXPECT_EQ(nostromo.setFeeReserveGuardConfig(stranger, 500ULL, 300ULL).errorCode, NOST::EAuctionError::Forbidden); + + EXPECT_EQ(nostromo.setFeeReserveGuardConfig(ContractTestingNOST::takeoverCoordinatorWallet(), 0ULL, 300ULL).errorCode, + NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.setFeeReserveGuardConfig(ContractTestingNOST::takeoverCoordinatorWallet(), NOST_BASIS_POINTS_SCALE + 1ULL, 300ULL).errorCode, + NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.setFeeReserveGuardConfig(ContractTestingNOST::takeoverCoordinatorWallet(), 500ULL, 0ULL).errorCode, + NOST::EAuctionError::InvalidInput); + NOST::GetFeeReserveGuardState_output guardState = nostromo.getFeeReserveGuardState(); + EXPECT_EQ(guardState.dropBasisPoints, defaultGuardState.dropBasisPoints); + EXPECT_EQ(guardState.windowSeconds, defaultGuardState.windowSeconds); + + EXPECT_EQ(nostromo.setFeeReserveGuardConfig(ContractTestingNOST::takeoverCoordinatorWallet(), 500ULL, 300ULL).errorCode, + NOST::EAuctionError::Success); + guardState = nostromo.getFeeReserveGuardState(); + EXPECT_EQ(guardState.dropBasisPoints, 500ULL); + EXPECT_EQ(guardState.windowSeconds, 300ULL); + + EXPECT_EQ(nostromo.setFeeReserveGuardConfig(ContractTestingNOST::managementWallet(), 800ULL, 400ULL).errorCode, NOST::EAuctionError::Success); + guardState = nostromo.getFeeReserveGuardState(); + EXPECT_EQ(guardState.dropBasisPoints, 800ULL); + EXPECT_EQ(guardState.windowSeconds, 400ULL); +} + +TEST(ContractNostromoAuction, EndEpochDistributesPendingFeesWhileEmergencyPausedAuction) +{ + ContractTestingNOST nostromo; + const id seller(1201, 1202, 1203, 1204); + const Asset asset{seller, assetNameFromString("GRDEPO")}; + + EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); + const NOST::CreateAuction_output& createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 3, 1)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + + const uint64 poolBefore = nostromo.getPendingServiceFeePool().pendingServiceFeePool; + EXPECT_GT(poolBefore, 0ULL); + + EXPECT_EQ(nostromo.setEmergencyPause(ContractTestingNOST::takeoverCoordinatorWallet(), true).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 1); + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, 0ULL); -TEST(TestContractNostromo, registerAndLogoutAndUpgradeFromTierChecker) -{ - ContractTestingNostromo nostromoTestCaseA; - - std::map duplicatedUser; - auto registers = getRandomUsers(10000, 10000); - - uint32 countOfRegister = 0, totalPoolWeight = 0; - uint64 totalDepositedQubic = 0, totalLogoutFeeAmount = 0; - - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - uint32 tierLevel = (uint32)random(1, 5); - uint64 depositeAmount, upgradeDeltaDepositeAmount; - switch (tierLevel) - { - case 1: - depositeAmount = NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT; - upgradeDeltaDepositeAmount = NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT - NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT; - totalLogoutFeeAmount += NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT * NOSTROMO_TIER_CHESTBURST_UNSTAKE_FEE / 100; - totalPoolWeight += NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; - break; - case 2: - depositeAmount = NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT; - upgradeDeltaDepositeAmount = NOSTROMO_TIER_DOG_STAKE_AMOUNT - NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT; - totalLogoutFeeAmount += NOSTROMO_TIER_DOG_STAKE_AMOUNT * NOSTROMO_TIER_DOG_UNSTAKE_FEE / 100; - totalPoolWeight += NOSTROMO_TIER_DOG_POOL_WEIGHT; - break; - case 3: - depositeAmount = NOSTROMO_TIER_DOG_STAKE_AMOUNT; - upgradeDeltaDepositeAmount = NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT - NOSTROMO_TIER_DOG_STAKE_AMOUNT; - totalLogoutFeeAmount += NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT * NOSTROMO_TIER_XENOMORPH_UNSTAKE_FEE / 100; - totalPoolWeight += NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; - break; - case 4: - depositeAmount = NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT; - upgradeDeltaDepositeAmount = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT - NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT; - totalLogoutFeeAmount += NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT * NOSTROMO_TIER_WARRIOR_UNSTAKE_FEE / 100; - totalPoolWeight += NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; - break; - case 5: - depositeAmount = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT; - totalLogoutFeeAmount += NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT * NOSTROMO_TIER_WARRIOR_UNSTAKE_FEE / 100; - totalPoolWeight += NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; - break; - default: - break; - } - // Register Tier - totalDepositedQubic += depositeAmount; - increaseEnergy(user, depositeAmount); - nostromoTestCaseA.registerInTier(user, tierLevel, depositeAmount); - nostromoTestCaseA.getState()->registerChecker(user, tierLevel, countOfRegister); - // Upgrade Tier - totalDepositedQubic += upgradeDeltaDepositeAmount; - increaseEnergy(user, upgradeDeltaDepositeAmount); - nostromoTestCaseA.upgradeTier(user, tierLevel + 1, upgradeDeltaDepositeAmount); - - if (tierLevel == 5) - { - nostromoTestCaseA.getState()->registerChecker(user, tierLevel, countOfRegister); - } - else - { - nostromoTestCaseA.getState()->registerChecker(user, tierLevel + 1, countOfRegister); - } - - duplicatedUser[user] = 1; - countOfRegister++; - } - nostromoTestCaseA.getState()->countOfRegisterChecker(countOfRegister); - nostromoTestCaseA.getState()->epochRevenueChecker(0); - nostromoTestCaseA.getState()->totalPoolWeightChecker(totalPoolWeight); - EXPECT_EQ(totalDepositedQubic, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); - - duplicatedUser.clear(); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - // Logout From Tier - nostromoTestCaseA.logoutFromTier(user); - duplicatedUser[user] = 1; - nostromoTestCaseA.getState()->logoutFromTierChecker(user); - } - EXPECT_EQ(totalLogoutFeeAmount, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); - nostromoTestCaseA.getState()->countOfRegisterChecker(0); - nostromoTestCaseA.getState()->epochRevenueChecker(totalLogoutFeeAmount); - nostromoTestCaseA.getState()->totalPoolWeightChecker(0); -} - -TEST(TestContractNostromo, createProjectAndVoteInProjectChecker) -{ - ContractTestingNostromo nostromoTestCaseB; - - auto registers = getRandomUsers(1000, 1000); - - // Register in each Tiers - increaseEnergy(registers[0], NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE); - nostromoTestCaseB.registerInTier(registers[0], 1, NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT); - - increaseEnergy(registers[1], NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE); - nostromoTestCaseB.registerInTier(registers[1], 2, NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT); - - increaseEnergy(registers[2], NOSTROMO_TIER_DOG_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE); - nostromoTestCaseB.registerInTier(registers[2], 3, NOSTROMO_TIER_DOG_STAKE_AMOUNT); - - increaseEnergy(registers[3], NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE); - nostromoTestCaseB.registerInTier(registers[3], 4, NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT); - - increaseEnergy(registers[4], NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE); - nostromoTestCaseB.registerInTier(registers[4], 5, NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT); - - setMemory(utcTime, 0); - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 12; - utcTime.Hour = 0; - updateQpiTime(); - - uint64 assetName = assetNameFromString("AAAA"); - - // This creation should be failed because there is no qualified to create the project. - nostromoTestCaseB.createProject(registers[0], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); - nostromoTestCaseB.getState()->numberOfCreatedProjectChecker(0); - nostromoTestCaseB.getState()->epochRevenueChecker(0); - EXPECT_EQ(getBalance(registers[0]), NOSTROMO_CREATE_PROJECT_FEE); - - // This creation should be failed because there is no qualified to create the project. - assetName = assetNameFromString("BBBB"); - nostromoTestCaseB.createProject(registers[1], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); - nostromoTestCaseB.getState()->numberOfCreatedProjectChecker(0); - nostromoTestCaseB.getState()->epochRevenueChecker(0); - EXPECT_EQ(getBalance(registers[1]), NOSTROMO_CREATE_PROJECT_FEE); - - // This creation should be failed because there is no qualified to create the project. - assetName = assetNameFromString("CCCC"); - nostromoTestCaseB.createProject(registers[2], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); - nostromoTestCaseB.getState()->numberOfCreatedProjectChecker(0); - nostromoTestCaseB.getState()->epochRevenueChecker(0); - EXPECT_EQ(getBalance(registers[2]), NOSTROMO_CREATE_PROJECT_FEE); - - - //This creation should be succeed because there is a qualified to create the project. - assetName = assetNameFromString("DDDD"); - nostromoTestCaseB.createProject(registers[3], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); - nostromoTestCaseB.getState()->numberOfCreatedProjectChecker(1); - nostromoTestCaseB.getState()->epochRevenueChecker(NOSTROMO_CREATE_PROJECT_FEE); - nostromoTestCaseB.getState()->createdProjectChecker(0, registers[3], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); - EXPECT_EQ(getBalance(registers[3]), 0); - - // This creation should be succeed because there is a qualified to create the project. - assetName = assetNameFromString("EEEE"); - nostromoTestCaseB.createProject(registers[4], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); - nostromoTestCaseB.getState()->numberOfCreatedProjectChecker(2); - nostromoTestCaseB.getState()->epochRevenueChecker(NOSTROMO_CREATE_PROJECT_FEE * 2); - nostromoTestCaseB.getState()->createdProjectChecker(1, registers[4], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); - EXPECT_EQ(getBalance(registers[4]), 0); - - // checkTokenCreatability function checker - EXPECT_EQ(nostromoTestCaseB.checkTokenCreatability(assetName).result, 1); - assetName = assetNameFromString("ABCD"); - EXPECT_EQ(nostromoTestCaseB.checkTokenCreatability(assetName).result, 0); - - setMemory(utcTime, 0); - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 13; - utcTime.Hour = 0; - updateQpiTime(); - - Array votedList; - - nostromoTestCaseB.voteInProject(registers[0], 0, 0); - votedList.set(0, 0); - nostromoTestCaseB.voteInProject(registers[1], 0, 1); - nostromoTestCaseB.voteInProject(registers[2], 0, 1); - nostromoTestCaseB.voteInProject(registers[3], 0, 1); - nostromoTestCaseB.voteInProject(registers[4], 0, 0); - - nostromoTestCaseB.getState()->voteInProjectChecker(0, 3, 2); - nostromoTestCaseB.getState()->numberOfVotedProjectAndVotedListChecker(registers[0], 1, votedList); - - // This vote should be failed. - nostromoTestCaseB.voteInProject(registers[0], 0, 0); - nostromoTestCaseB.getState()->voteInProjectChecker(0, 3, 2); - nostromoTestCaseB.getState()->numberOfVotedProjectAndVotedListChecker(registers[0], 1, votedList); - - // This vote should be succeed. - nostromoTestCaseB.voteInProject(registers[0], 1, 0); - votedList.set(1, 1); - nostromoTestCaseB.getState()->voteInProjectChecker(1, 0, 1); - nostromoTestCaseB.getState()->numberOfVotedProjectAndVotedListChecker(registers[0], 2, votedList); - - nostromoTestCaseB.voteInProject(registers[1], 1, 1); - nostromoTestCaseB.voteInProject(registers[2], 1, 1); - nostromoTestCaseB.voteInProject(registers[3], 1, 1); - nostromoTestCaseB.voteInProject(registers[4], 1, 1); - nostromoTestCaseB.getState()->voteInProjectChecker(1, 4, 1); -} - -TEST(TestContractNostromo, createFundraisingAndInvestInProjectAndClaimTokenChecker) -{ - uint64 epochRevenu_t = 0; - uint32 numberOfCreatedProject_t = 0; - uint32 numberOfFundraising_t = 0;; - - ContractTestingNostromo nostromoTestCaseC; - - auto registers = getRandomUsers(10000, 10000); - - setMemory(utcTime, 0); - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 11; - utcTime.Hour = 0; - updateQpiTime(); - - increaseEnergy(registers[0], NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT + NOSTROMO_CREATE_PROJECT_FEE + NOSTROMO_QX_TOKEN_ISSUANCE_FEE); - nostromoTestCaseC.registerInTier(registers[0], 5, NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT); - uint64 assetName = assetNameFromString("GGGG"); - nostromoTestCaseC.createProject(registers[0], assetName, 21000000, 25, 6, 13, 0, 25, 6, 15, 0); - - // getProjectByIndex function Checker - NOST::getProjectByIndex_output getProjectByIndex_output = nostromoTestCaseC.getProjectByIndex(0); - - EXPECT_EQ(getProjectByIndex_output.project.creator, registers[0]); - uint32 tmpDate; - NOST::packNostromoDate(25, 6, 15, 0, 0, 0, tmpDate); - EXPECT_EQ(getProjectByIndex_output.project.endDate , tmpDate); - EXPECT_EQ(getProjectByIndex_output.project.isCreatedFundarasing , 0); - EXPECT_EQ(getProjectByIndex_output.project.numberOfNo, 0); - EXPECT_EQ(getProjectByIndex_output.project.numberOfYes, 0); - NOST::packNostromoDate(25, 6, 13, 0, 0, 0, tmpDate); - EXPECT_EQ(getProjectByIndex_output.project.startDate, tmpDate); - EXPECT_EQ(getProjectByIndex_output.project.supplyOfToken, 21000000); - EXPECT_EQ(getProjectByIndex_output.project.tokenName, assetName); - - numberOfCreatedProject_t++; - epochRevenu_t += 100000000; - - std::map duplicatedUser; - uint64 totalPoolWeight = NOSTROMO_TIER_WARRIOR_POOL_WEIGHT, totalDepositedQubic = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT; - uint32 countOfRegister = 0; - - for (const auto& user : registers) - { - if (countOfRegister == 0) - { - countOfRegister++; - continue; - } - - if (duplicatedUser[user]) - { - continue; - } - uint8 tierLevel = (uint8)random(1, 5); - uint64 depositeAmount, userPoolWeight; - switch (tierLevel) - { - case 1: - depositeAmount = NOSTROMO_TIER_FACEHUGGER_STAKE_AMOUNT; - totalPoolWeight += NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT; - userPoolWeight = NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT; - break; - case 2: - depositeAmount = NOSTROMO_TIER_CHESTBURST_STAKE_AMOUNT; - totalPoolWeight += NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; - userPoolWeight = NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT; - break; - case 3: - depositeAmount = NOSTROMO_TIER_DOG_STAKE_AMOUNT; - totalPoolWeight += NOSTROMO_TIER_DOG_POOL_WEIGHT; - userPoolWeight = NOSTROMO_TIER_DOG_POOL_WEIGHT; - break; - case 4: - depositeAmount = NOSTROMO_TIER_XENOMORPH_STAKE_AMOUNT; - totalPoolWeight += NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; - userPoolWeight = NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT; - break; - case 5: - depositeAmount = NOSTROMO_TIER_WARRIOR_STAKE_AMOUNT; - totalPoolWeight += NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; - userPoolWeight = NOSTROMO_TIER_WARRIOR_POOL_WEIGHT; - break; - default: - break; - } - - // Register Tier - totalDepositedQubic += depositeAmount; - increaseEnergy(user, depositeAmount); - nostromoTestCaseC.registerInTier(user, tierLevel, depositeAmount); - - duplicatedUser[user] = 1; - countOfRegister++; - - // getTierLevelByUser function Checker - EXPECT_EQ(nostromoTestCaseC.getTierLevelByUser(user).tierLevel, tierLevel); - } - - // Vote in Project - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 14; - utcTime.Hour = 0; - updateQpiTime(); - - uint32 Ynumber = 0, Nnumber = 0; - duplicatedUser.clear(); - - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - - bit decision = (bit)random(0, 3); - if (decision) - { - Ynumber++; - } - else - { - Nnumber++; - } - - nostromoTestCaseC.voteInProject(user, 0, decision); - duplicatedUser[user] = 1; - } - nostromoTestCaseC.getState()->voteInProjectChecker(0, Ynumber, Nnumber); - - // Create the Fundraising - // This fundraising should not be created because the voting is not finished yet. - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 14; - utcTime.Hour = 0; - updateQpiTime(); - - nostromoTestCaseC.createFundraising(registers[0], 100, 2000000, 150000000, 0, - 25, 6, 17, 0, - 25, 6, 25, 0, - 25, 6, 28, 0, - 25, 7, 1, 0, - 25, 7, 10, 0, - 25, 7, 15, 0, - 25, 7, 25, 0, - 25, 7, 27, 0, - 26, 7, 27, 0, - 20, 10, 12); - - nostromoTestCaseC.getState()->countOfFundraisingChecker(0); - - // It should be created. - - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 16; - utcTime.Hour = 0; - updateQpiTime(); - - nostromoTestCaseC.createFundraising(registers[0], 100000, 2000000, 150000000000, 0, - 25, 6, 17, 0, - 25, 6, 25, 0, - 25, 6, 28, 0, - 25, 7, 1, 0, - 25, 7, 10, 0, - 25, 7, 15, 0, - 25, 7, 25, 0, - 25, 7, 27, 0, - 26, 7, 27, 0, - 20, 10, 12); - numberOfFundraising_t++; - - nostromoTestCaseC.getState()->countOfFundraisingChecker(1); - nostromoTestCaseC.getState()->createFundraisingChecker(registers[0], 100000, 2000000, 150000000000, 0, - 25, 6, 17, 0, - 25, 6, 25, 0, - 25, 6, 28, 0, - 25, 7, 1, 0, - 25, 7, 10, 0, - 25, 7, 15, 0, - 25, 7, 25, 0, - 25, 7, 27, 0, - 26, 7, 27, 0, - 20, 10, 12, 0); - - // getFundarasingByIndex function checker - NOST::getFundarasingByIndex_output getFundarasingByIndex_output = nostromoTestCaseC.getFundarasingByIndex(0); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.indexOfProject, 0); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.isCreatedToken, 0); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.raisedFunds, 0); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.requiredFunds, 150000000000); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.soldAmount, 2000000); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.stepOfVesting, 12); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.TGE, 10); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.threshold, 20); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.tokenPrice, 100000); - NOST::packNostromoDate(25, 6, 17, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.firstPhaseStartDate, tmpDate); - NOST::packNostromoDate(25, 6, 25, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.firstPhaseEndDate, tmpDate); - NOST::packNostromoDate(25, 6, 28, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.secondPhaseStartDate, tmpDate); - NOST::packNostromoDate(25, 7, 1, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.secondPhaseEndDate, tmpDate); - NOST::packNostromoDate(25, 7, 10, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.thirdPhaseStartDate, tmpDate); - NOST::packNostromoDate(25, 7, 15, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.thirdPhaseEndDate, tmpDate); - NOST::packNostromoDate(25, 7, 25, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.listingStartDate, tmpDate); - NOST::packNostromoDate(25, 7, 27, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.cliffEndDate, tmpDate); - NOST::packNostromoDate(26, 7, 27, 0, 0, 0, tmpDate); - EXPECT_EQ(getFundarasingByIndex_output.fundarasing.vestingEndDate, tmpDate); - - // Phase 1 Investment - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 17; - utcTime.Hour = 1; - updateQpiTime(); - - uint64 facehuggerMaxInvestAmount = 180000000000 * NOSTROMO_TIER_FACEHUGGER_POOL_WEIGHT / totalPoolWeight; - uint64 chestburstMaxInvestAmount = 180000000000 * NOSTROMO_TIER_CHESTBURST_POOL_WEIGHT / totalPoolWeight; - uint64 dogMaxInvestAmount = 180000000000 * NOSTROMO_TIER_DOG_POOL_WEIGHT / totalPoolWeight; - uint64 xenomorphMaxInvestAmount = 180000000000 * NOSTROMO_TIER_XENOMORPH_POOL_WEIGHT / totalPoolWeight; - uint64 warriorMaxInvestAmount = 180000000000 * NOSTROMO_TIER_WARRIOR_POOL_WEIGHT / totalPoolWeight; - - uint64 totalInvestedAmount = 0; - duplicatedUser.clear(); - uint32 ct = 0; - uint32 overDeposit = 1000; // it should be ignored - uint64 originalSCBalance = getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0)); - - std::map investedAmountMP; - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - ct++; - continue; - } - ct++; - increaseEnergy(user, 180000000000); - uint8 tierLevel = nostromoTestCaseC.getState()->getTierLevel(user); - - if (ct >= 4000) - { - // Phase 2 Investment - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 29; - utcTime.Hour = 0; - updateQpiTime(); - } - - switch (tierLevel) - { - case 1: - if (ct < 4000) - { - totalInvestedAmount += facehuggerMaxInvestAmount; - investedAmountMP[user] += facehuggerMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 0, facehuggerMaxInvestAmount + overDeposit); - break; - case 2: - if (ct < 4000) - { - totalInvestedAmount += chestburstMaxInvestAmount; - investedAmountMP[user] += chestburstMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 0, chestburstMaxInvestAmount + overDeposit); - break; - case 3: - if (ct < 4000) - { - totalInvestedAmount += dogMaxInvestAmount; - investedAmountMP[user] += dogMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 0, dogMaxInvestAmount + overDeposit); - break; - case 4: - totalInvestedAmount += xenomorphMaxInvestAmount; - investedAmountMP[user] += xenomorphMaxInvestAmount; - nostromoTestCaseC.investInProject(user, 0, xenomorphMaxInvestAmount + overDeposit); - break; - case 5: - totalInvestedAmount += warriorMaxInvestAmount; - investedAmountMP[user] += warriorMaxInvestAmount; - nostromoTestCaseC.investInProject(user, 0, warriorMaxInvestAmount + overDeposit); - break; - - default: - break; - } - - duplicatedUser[user] = 1; - } - - nostromoTestCaseC.getState()->totalRaisedFundChecker(0, totalInvestedAmount, assetName); - EXPECT_EQ(originalSCBalance + totalInvestedAmount - NOSTROMO_QX_TOKEN_ISSUANCE_FEE, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); - - // Phase 3 Investment - utcTime.Year = 2025; - utcTime.Month = 7; - utcTime.Day = 11; - utcTime.Hour = 0; - updateQpiTime(); - - uint64 amount = 10000000; - duplicatedUser.clear(); - ct = 0; - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - ct++; - uint8 tierLevel = nostromoTestCaseC.getState()->getTierLevel(user); - increaseEnergy(user, amount); - nostromoTestCaseC.investInProject(user, 0, amount); - if (totalInvestedAmount + amount < 180000000000) - { - totalInvestedAmount += amount; - investedAmountMP[user] += amount; - - // getNumberOfInvestedProjects function checker - NOST::getNumberOfInvestedProjects_output getNumberOfInvestedProjects_output = nostromoTestCaseC.getNumberOfInvestedProjects(user); - - EXPECT_EQ(getNumberOfInvestedProjects_output.numberOfInvestedProjects, 1); - } - duplicatedUser[user] = 1; - } - - nostromoTestCaseC.getState()->totalRaisedFundChecker(0, totalInvestedAmount, assetName); - EXPECT_EQ(originalSCBalance + totalInvestedAmount - NOSTROMO_QX_TOKEN_ISSUANCE_FEE, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); - - // getMaxClaimAmount function checker - utcTime.Year = 2025; - utcTime.Month = 7; - utcTime.Day = 26; - utcTime.Hour = 0; - updateQpiTime(); - - duplicatedUser.clear(); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - - EXPECT_EQ(nostromoTestCaseC.getMaxClaimAmount(user, 0), investedAmountMP[user] / 100000 * 10 / 100); - - duplicatedUser[user] = 1; - } - - utcTime.Year = 2025; - utcTime.Month = 8; - utcTime.Day = 5; - utcTime.Hour = 0; - updateQpiTime(); - - duplicatedUser.clear(); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - - EXPECT_EQ(nostromoTestCaseC.getMaxClaimAmount(user, 0), investedAmountMP[user] / 100000 * (10 + 7) / 100); - - duplicatedUser[user] = 1; - } - - utcTime.Year = 2026; - utcTime.Month = 8; - utcTime.Day = 5; - utcTime.Hour = 0; - updateQpiTime(); - - duplicatedUser.clear(); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - - EXPECT_EQ(nostromoTestCaseC.getMaxClaimAmount(user, 0), investedAmountMP[user] / 100000); - - duplicatedUser[user] = 1; - } - - // claimToken Checker - std::map claimedAmountMP; - for (uint32 i = 1; i <= 12; i++) - { - if (i >= 6) - { - utcTime.Year = 2026; - } - utcTime.Month = (7 + i) % 12; - if (utcTime.Month == 0) utcTime.Month = 12; - utcTime.Day = 5; - utcTime.Hour = 0; - updateQpiTime(); - - duplicatedUser.clear(); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - - uint64 investedAmount = nostromoTestCaseC.getState()->getInvestedAmount(0, user); - uint64 claimAmount = investedAmount / 100000 / 12; - claimedAmountMP[user] += nostromoTestCaseC.claimToken(user, claimAmount, 0); - - duplicatedUser[user] = 1; - } - } - - // getInfoUserInvested function checker - duplicatedUser.clear(); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - - duplicatedUser[user] = 1; - - NOST::getInfoUserInvested_output getInfoUserInvested_output = nostromoTestCaseC.getInfoUserInvested(user); - EXPECT_EQ(getInfoUserInvested_output.listUserInvested.get(0).indexOfFundraising, 0); - EXPECT_EQ(getInfoUserInvested_output.listUserInvested.get(0).investedAmount, investedAmountMP[user]); - EXPECT_EQ(getInfoUserInvested_output.listUserInvested.get(0).claimedAmount, claimedAmountMP[user]); - } - - // Checking to remove element after claiming the max amount - utcTime.Year = 2026; - utcTime.Month = 8; - utcTime.Day = 5; - utcTime.Hour = 0; - updateQpiTime(); - - duplicatedUser.clear(); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - uint64 claimAmount = nostromoTestCaseC.getMaxClaimAmount(user, 0) - claimedAmountMP[user]; - claimedAmountMP[user] += nostromoTestCaseC.claimToken(user, claimAmount, 0); - - duplicatedUser[user] = 1; - - nostromoTestCaseC.getState()->removeElementAfterClaimChecker(user); - } - - ct = 0; - duplicatedUser.clear(); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - if (ct == 0) - { - EXPECT_EQ(numberOfPossessedShares(assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), user, user, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX) - 19000000, claimedAmountMP[user]); - } - else - { - EXPECT_EQ(numberOfPossessedShares(assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), user, user, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX), claimedAmountMP[user]); - } - ct++; - duplicatedUser[user] = 1; - } - - // transferShareManagementRights Checker - increaseEnergy(registers[0], 1000000); - - Asset asset; - asset.assetName = assetName; - asset.issuer = id(NOST_CONTRACT_INDEX, 0, 0, 0); - EXPECT_EQ(nostromoTestCaseC.TransferShareManagementRights(registers[0], asset, 10000, QX_CONTRACT_INDEX), 10000); - EXPECT_EQ(numberOfPossessedShares(asset.assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), registers[0], registers[0], QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), 10000); - - // EndEpochSucceedFundraising Checker - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 20; - utcTime.Hour = 0; - updateQpiTime(); - - increaseEnergy(registers[0], NOSTROMO_CREATE_PROJECT_FEE); - assetName = assetNameFromString("AAAA"); - nostromoTestCaseC.createProject(registers[0], assetName, 21000000, 25, 6, 22, 0, 25, 6, 25, 0); - numberOfCreatedProject_t++; - epochRevenu_t += 100000000; - - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 23; - utcTime.Hour = 0; - updateQpiTime(); - - Ynumber = 0; Nnumber = 0; - duplicatedUser.clear(); - - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - - bit decision = (bit)random(0, 3); - if (decision) - { - Ynumber++; - } - else - { - Nnumber++; - } - - nostromoTestCaseC.voteInProject(user, 1, decision); - duplicatedUser[user] = 1; - - // getUserVoteStatus function Checker - NOST::getUserVoteStatus_output getUserVoteStatus_output = nostromoTestCaseC.getUserVoteStatus(user); - EXPECT_EQ(getUserVoteStatus_output.numberOfVotedProjects, 2); - EXPECT_EQ(getUserVoteStatus_output.projectIndexList.get(0), 0); - EXPECT_EQ(getUserVoteStatus_output.projectIndexList.get(1), 1); - } - nostromoTestCaseC.getState()->voteInProjectChecker(1, Ynumber, Nnumber); - - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 26; - utcTime.Hour = 0; - updateQpiTime(); - increaseEnergy(registers[0], NOSTROMO_QX_TOKEN_ISSUANCE_FEE); - - nostromoTestCaseC.createFundraising(registers[0], 100000, 2000000, 150000000000, 1, - 25, 6, 27, 0, - 25, 7, 5, 0, - 25, 7, 8, 0, - 25, 7, 10, 0, - 25, 7, 20, 0, - 25, 7, 23, 0, - 25, 7, 25, 0, - 25, 7, 27, 0, - 26, 7, 27, 0, - 20, 10, 12); - numberOfFundraising_t++; - - nostromoTestCaseC.getState()->countOfFundraisingChecker(2); - nostromoTestCaseC.getState()->createFundraisingChecker(registers[0], 100000, 2000000, 150000000000, 1, - 25, 6, 27, 0, - 25, 7, 5, 0, - 25, 7, 8, 0, - 25, 7, 10, 0, - 25, 7, 20, 0, - 25, 7, 23, 0, - 25, 7, 25, 0, - 25, 7, 27, 0, - 26, 7, 27, 0, - 20, 10, 12, 1); - - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 27; - utcTime.Hour = 1; - updateQpiTime(); - - uint64 totalInvestedAmount_2 = 0; - duplicatedUser.clear(); - ct = 0; - originalSCBalance = getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0)); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - ct++; - continue; - } - ct++; - increaseEnergy(user, 180000000000); - uint8 tierLevel = nostromoTestCaseC.getState()->getTierLevel(user); - - if (ct >= 4000) - { - - // Phase 2 Investment - utcTime.Year = 2025; - utcTime.Month = 7; - utcTime.Day = 9; - utcTime.Hour = 0; - updateQpiTime(); - } - - switch (tierLevel) - { - case 1: - if (ct < 4000) - { - totalInvestedAmount_2 += facehuggerMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 1, facehuggerMaxInvestAmount); - break; - case 2: - if (ct < 4000) - { - totalInvestedAmount_2 += chestburstMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 1, chestburstMaxInvestAmount); - break; - case 3: - if (ct < 4000) - { - totalInvestedAmount_2 += dogMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 1, dogMaxInvestAmount); - break; - case 4: - totalInvestedAmount_2 += xenomorphMaxInvestAmount; - nostromoTestCaseC.investInProject(user, 1, xenomorphMaxInvestAmount); - break; - case 5: - totalInvestedAmount_2 += warriorMaxInvestAmount; - nostromoTestCaseC.investInProject(user, 1, warriorMaxInvestAmount); - break; - - default: - break; - } - - duplicatedUser[user] = 1; - } - - nostromoTestCaseC.getState()->totalRaisedFundChecker(1, totalInvestedAmount_2, assetName); - EXPECT_EQ(originalSCBalance + totalInvestedAmount_2 - NOSTROMO_QX_TOKEN_ISSUANCE_FEE, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); - - // getStats function Checker - nostromoTestCaseC.getState()->getStatsChecker(epochRevenu_t, totalPoolWeight, numberOfCreatedProject_t, numberOfFundraising_t, countOfRegister); - - utcTime.Year = 2025; - utcTime.Month = 7; - utcTime.Day = 24; - utcTime.Hour = 0; - updateQpiTime(); - - uint64 originalCreatorBalance = getBalance(registers[0]); - nostromoTestCaseC.endEpoch(); - EXPECT_EQ(getBalance(registers[0]) - originalCreatorBalance, totalInvestedAmount - div(totalInvestedAmount * 5, 100ULL) + totalInvestedAmount_2 - div(totalInvestedAmount_2 * 5, 100ULL)); - nostromoTestCaseC.getState()->endEpochSucceedFundraisingChecker(registers[0], 1, totalInvestedAmount_2, originalCreatorBalance, assetName); - - // EndEpochFailedFundraising Checker - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 20; - utcTime.Hour = 0; - updateQpiTime(); - - increaseEnergy(registers[0], NOSTROMO_CREATE_PROJECT_FEE); - assetName = assetNameFromString("BBBB"); - nostromoTestCaseC.createProject(registers[0], assetName, 21000000, 25, 6, 22, 0, 25, 6, 25, 0); - numberOfCreatedProject_t++; - epochRevenu_t += 100000000; - - // getProjectIndexListByCreator function checker - NOST::getProjectIndexListByCreator_output getProjectIndexListByCreator_output = nostromoTestCaseC.getProjectIndexListByCreator(registers[0]); - for (uint32 i = 0; i < 128; i++) - { - if (i < 3) - { - EXPECT_EQ(getProjectIndexListByCreator_output.indexListForProjects.get(i), i); - } - else { - EXPECT_EQ(getProjectIndexListByCreator_output.indexListForProjects.get(i), 262144); - } - } - - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 23; - utcTime.Hour = 0; - updateQpiTime(); - - Ynumber = 0; Nnumber = 0; - duplicatedUser.clear(); - - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - continue; - } - - bit decision = (bit)random(0, 3); - if (decision) - { - Ynumber++; - } - else - { - Nnumber++; - } - - nostromoTestCaseC.voteInProject(user, 2, decision); - duplicatedUser[user] = 1; - } - nostromoTestCaseC.getState()->voteInProjectChecker(2, Ynumber, Nnumber); - - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 26; - utcTime.Hour = 0; - updateQpiTime(); - increaseEnergy(registers[0], NOSTROMO_QX_TOKEN_ISSUANCE_FEE); - - nostromoTestCaseC.createFundraising(registers[0], 100000, 2000000, 150000000000, 2, - 25, 6, 27, 0, - 25, 7, 5, 0, - 25, 7, 8, 0, - 25, 7, 10, 0, - 25, 7, 20, 0, - 25, 7, 23, 0, - 25, 7, 25, 0, - 25, 7, 27, 0, - 26, 7, 27, 0, - 20, 10, 12); - numberOfFundraising_t++; - - nostromoTestCaseC.getState()->countOfFundraisingChecker(3); - nostromoTestCaseC.getState()->createFundraisingChecker(registers[0], 100000, 2000000, 150000000000, 2, - 25, 6, 27, 0, - 25, 7, 5, 0, - 25, 7, 8, 0, - 25, 7, 10, 0, - 25, 7, 20, 0, - 25, 7, 23, 0, - 25, 7, 25, 0, - 25, 7, 27, 0, - 26, 7, 27, 0, - 20, 10, 12, 2); - - utcTime.Year = 2025; - utcTime.Month = 6; - utcTime.Day = 27; - utcTime.Hour = 1; - updateQpiTime(); - - uint64 totalInvestedAmount_3 = 0; - duplicatedUser.clear(); - ct = 0; - originalSCBalance = getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0)); - for (const auto& user : registers) - { - if (duplicatedUser[user]) - { - ct++; - continue; - } - ct++; - increaseEnergy(user, 180000000000); - uint8 tierLevel = nostromoTestCaseC.getState()->getTierLevel(user); - - if (ct >= 4000) - { - - // Phase 2 Investment - utcTime.Year = 2025; - utcTime.Month = 7; - utcTime.Day = 9; - utcTime.Hour = 0; - updateQpiTime(); - } - - bit sg = 0; - switch (tierLevel) - { - case 1: - if (ct < 4000) - { - if (totalInvestedAmount_3 + facehuggerMaxInvestAmount > 120000000000) - { - sg = 1; - break; - } - totalInvestedAmount_3 += facehuggerMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 2, facehuggerMaxInvestAmount); - break; - case 2: - if (ct < 4000) - { - if (totalInvestedAmount_3 + chestburstMaxInvestAmount > 120000000000) - { - sg = 1; - break; - } - totalInvestedAmount_3 += chestburstMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 2, chestburstMaxInvestAmount); - break; - case 3: - if (ct < 4000) - { - if (totalInvestedAmount_3 + dogMaxInvestAmount > 120000000000) - { - sg = 1; - break; - } - totalInvestedAmount_3 += dogMaxInvestAmount; - } - nostromoTestCaseC.investInProject(user, 2, dogMaxInvestAmount); - break; - case 4: - if (totalInvestedAmount_3 + xenomorphMaxInvestAmount > 120000000000) - { - sg = 1; - break; - } - totalInvestedAmount_3 += xenomorphMaxInvestAmount; - nostromoTestCaseC.investInProject(user, 2, xenomorphMaxInvestAmount); - break; - case 5: - if (totalInvestedAmount_3 + warriorMaxInvestAmount > 120000000000) - { - sg = 1; - break; - } - totalInvestedAmount_3 += warriorMaxInvestAmount; - nostromoTestCaseC.investInProject(user, 2, warriorMaxInvestAmount); - break; - - default: - break; - } - - if (sg) - { - break; - } - - duplicatedUser[user] = 1; - } - - nostromoTestCaseC.getState()->totalRaisedFundChecker(2, totalInvestedAmount_3, assetName); - - utcTime.Year = 2025; - utcTime.Month = 7; - utcTime.Day = 24; - utcTime.Hour = 0; - updateQpiTime(); - - originalCreatorBalance = getBalance(registers[0]); - EXPECT_EQ(originalSCBalance + totalInvestedAmount_3, getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); - - uint64 epochRevenue = nostromoTestCaseC.getState()->getEpochRevenue(); - uint64 teamFee = div(epochRevenue, 10ULL); - epochRevenue -= teamFee; - nostromoTestCaseC.endEpoch(); - - EXPECT_EQ(originalSCBalance + totalInvestedAmount_3 - teamFee - (div(epochRevenue, 676ULL) * 676), getBalance(id(NOST_CONTRACT_INDEX, 0, 0, 0))); - nostromoTestCaseC.getState()->endEpochFailedFundraisingChecker(2); - nostromoTestCaseC.getState()->endEpochVoteStatusClearChecker(); + // The auction should remain paused after END_EPOCH; only a manual resume clears it. + EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 1); }