From 32582a4ecb92337c7f6764d39842776a76defcb3 Mon Sep 17 00:00:00 2001 From: N-010 Date: Tue, 14 Apr 2026 21:51:38 +0300 Subject: [PATCH 01/59] Refactor Nostromo contract: replace tier-based features with auction functionality, including participant management, lot handling, and visibility settings. --- src/contracts/Nostromo.h | 1915 ++++++++++++-------------------------- 1 file changed, 593 insertions(+), 1322 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 1e50441e1..c182420ce 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -1,29 +1,14 @@ 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; +constexpr uint64 NOST_AUCTION_NUM = 2048; +constexpr uint64 NOST_AUCTION_METADATA_CID_LENGTH = 64; +constexpr uint64 NOST_AUCTION_PARTICIPANT_NUM = 4096; +constexpr uint64 NOST_AUCTION_LOT_ITEM_NUM = 64; +constexpr uint64 NOST_AUCTION_ALLOWED_WALLET_NUM = 128; +constexpr uint64 NOST_PRIVATE_AUCTION_FEE = 50000000ULL; +constexpr uint64 NOST_AUCTION_EXTENSION_SECONDS = 300ULL; +constexpr uint64 NOST_SECONDS_PER_DAY = 86400ULL; +constexpr uint64 NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS = 604800ULL; struct NOST2 { @@ -31,330 +16,303 @@ 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 EAuctionType : uint8 { - if (A == B) return 0; - if (A < B) return -1; - return 1; - } + None, + Batch, + Standard + }; - /** - * @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) + enum class EAuctionVisibility : uint8 { - res = ((_year - 24) << 26) | (_month << 22) | (_day << 17) | (_hour << 12) | (_minute << 6) | (_second); - } + None, + Public, + Private + }; - 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 EAuctionStatus : uint8 { - return ((data >> 6) & 0b111111); - } - inline static uint32 NostGetSecond(uint32 data) - { - 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) - { - _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; - } - } - /** - * @return difference in number of second, A must be smaller than or equal B to have valid value - */ - inline static void diffDateInSecond(uint32& A, uint32& B, sint32& i, uint64& dayA, uint64& dayB, uint64& res) + Success, + InvalidInput, + AuctionNotFound, + AuctionClosed, + Forbidden, + InsufficientFunds, + InsufficientAssetBalance, + StorageFull, + InvalidAuctionType, + InvalidVisibility, + BidTooLow, + PrivateAuctionAccessDenied + }; + + struct AuctionParticipantKey { - 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); + id auctionId; + id participant; - // 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++) + bool operator<(const AuctionParticipantKey& rhs) const { - if (mod(i, 4) == 0) + if (auctionId < rhs.auctionId) + { + return true; + } + if (rhs.auctionId < auctionId) { - dayB++; + return false; } + return participant < rhs.participant; } - 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)); - } - - 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; - } - - /****** END PORTED TIMEUTILS FROM OLD Nostromo *****/ - - struct investInfo - { - uint64 investedAmount; - uint64 claimedAmount; - uint32 indexOfFundraising; }; - struct projectInfo + /** + * @brief Stores the active bid state of one wallet in one auction. + * @note The same struct is shared by batch and standard auctions. + */ + struct AuctionParticipantData { - id creator; - uint64 tokenName; - uint64 supplyOfToken; - uint32 startDate; - uint32 endDate; - uint32 numberOfYes; - uint32 numberOfNo; - bit isCreatedFundarasing; + uint64 escrowedAmount; + uint64 requestedQuantity; + uint64 allocatedQuantity; + uint64 pricePerUnit; + DateAndTime lastBidTime; + id participant; + uint8 isHighestBidder; + uint8 isWinningBid; }; - struct fundaraisingInfo + /** + * @brief Describes one asset entry inside an auction lot. + * @note One non-zero entry means a single-asset auction lot. + * @note Multiple non-zero entries mean a bundle of different assets. + */ + struct AuctionLotEntry { - 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; + /// Asset included in the lot. + Asset asset; + + /// Quantity of this asset included in the lot. + sint64 quantity; }; - struct StateData + /** + * @brief Stores all persistent data for one auction. + * @note The same struct is shared by batch and standard auctions. + * @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 AuctionData { - HashMap users; - HashMap, NOSTROMO_MAX_USER> voteStatus; - HashMap numberOfVotedProject; - HashSet tokens; + /// Unique identifier of the auction created in the Auction House. + id auctionId; - HashMap, NOSTROMO_MAX_USER> investors; - HashMap numberOfInvestedProjects; - Array tmpInvestedList; + /// Total sale units offered in this auction; batch auctions use asset quantity, standard auctions use one unit for the whole lot. + uint64 quantityForSale; - Array projects; + /// Quantity already assigned to winning bids after settlement. + uint64 allocatedQuantity; - Array fundaraisings; + /// Minimum quantity a bidder may request in a batch auction; standard auctions sell the entire lot as one unit. + uint64 minimumPurchaseQuantity; - id teamAddress; - sint64 transferRightsFee; - uint64 epochRevenue, totalPoolWeight; - uint32 numberOfRegister, numberOfCreatedProject, numberOfFundraising; - }; + /// Initial Price for a standard auction; bids cannot start below this value. + uint64 initialPricePerUnit; - struct registerInTier_input - { - uint32 tierLevel; - }; + /// Sale Price defined by the seller as the desired / minimum acceptable selling price. + uint64 salePricePerUnit; - struct registerInTier_output - { - uint32 tierLevel; - }; + /// Minimum step by which a new bid must exceed the current highest bid. + uint64 minimumBidIncrement; - struct logoutFromTier_input - { + /// Buy Now price that closes a standard auction immediately when matched or exceeded. + uint64 buyNowPricePerUnit; - }; + /// Highest price per unit currently offered by any active bid. + uint64 highestBidPerUnit; - struct logoutFromTier_output - { - bit result; - }; + /// Quantity requested by the current highest bid. + uint64 highestBidQuantity; - struct createProject_input - { - uint64 tokenName; - uint64 supply; - uint32 startYear; - uint32 startMonth; - uint32 startDay; - uint32 startHour; - uint32 endYear; - uint32 endMonth; - uint32 endDay; - uint32 endHour; - }; + /// Total amount escrowed by the current highest bid. + uint64 highestBidAmount; - struct createProject_output - { - uint32 indexOfProject; + /// Auction duration in seconds, derived from the duration configured in days. + uint64 auctionDurationSeconds; + + /// Timestamp when the seller created the auction. + DateAndTime createdAt; + + /// Timestamp of the most recent accepted bid. + DateAndTime lastBidAt; + + /// Deadline for the seller to manually accept or reject a bid after auction end when the highest bid is between Initial Price and Sale Price. + DateAndTime sellerDecisionDeadline; + + /// Timestamp when the auction was finalized, cancelled, or otherwise settled. + DateAndTime settledAt; + + /// Number of distinct bidders who have placed bids in this auction. + uint32 bidderCount; + + /// Wallet that created the auction and offers the asset for sale. + id seller; + + /// Wallet that currently holds the highest bid. + id highestBidder; + + /// Primary asset reference of the lot, equal to the first non-empty lot entry. + Asset assetForSale; + + /// Asset required for participation when the auction visibility is private. + Asset requiredAccessAsset; + + /// Auction lot contents; one non-empty entry means a single asset, multiple non-empty entries mean a bundle. + Array auctionLotItems; + + /// Wallet whitelist for private batch auctions; only these wallets may participate when wallet-based restriction is used. + HashSet allowedBidderWallets; + + /// IPFS CID stored in Pinata that points to the auction name and description metadata. + Array metadataIpfsCid; + + /// Auction House mode: Batch Auction or Standard Auction. + EAuctionType type; + + /// Auction visibility: public or restricted private access. + EAuctionVisibility visibility; + + /// Current lifecycle status of the auction, including the manual seller-decision phase. + EAuctionStatus status; }; - struct voteInProject_input + struct StateData { - uint32 indexOfProject; - bit decision; + HashMap auctionList; + HashMap participants; }; - struct voteInProject_output + struct CreateAuction_input { + /// IPFS CID stored in Pinata that points to the auction name and description metadata. + Array metadataIpfsCid; + + /// Auction lot contents; one non-empty entry means a single asset, multiple non-empty entries mean a bundle. + Array auctionLotItems; + /// Asset required to participate when the auction is configured as private and asset-based access is used. + Asset requiredAccessAsset; + + /// Wallet list for private batch auctions; copied into the auction whitelist on creation. + Array allowedBidderWallets; + + /// Minimum quantity a bidder may request in a batch auction; standard auctions sell the whole lot as one unit. + uint64 minimumPurchaseQuantity; + + /// Initial Price for a standard auction; bids cannot be placed below this value. + uint64 initialPricePerUnit; + + /// Sale Price defined by the seller as the desired / minimum acceptable selling price. + uint64 salePricePerUnit; + + /// Minimum step by which each new bid must exceed the current highest bid. + uint64 minimumBidIncrementPerUnit; + + /// Buy Now price that immediately closes a standard auction once matched or exceeded. + uint64 buyNowPricePerUnit; + + /// Auction duration configured by the seller in days. + uint32 durationDays; + + /// Auction House mode selected by the seller: Batch Auction or Standard Auction. + uint8 auctionType; + + /// Visibility selected by the seller: public or private. + uint8 auctionVisibility; }; - struct createFundraising_input + struct CreateAuction_output { - 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; + id auctionId; + uint8 errorCode; }; - struct createFundraising_output + struct PlaceBid_input { - + id auctionId; + uint64 quantity; + uint64 pricePerUnit; }; - struct investInProject_input + struct PlaceBid_output { - uint32 indexOfFundraising; + uint64 escrowedAmount; + uint64 refundedAmount; + uint8 errorCode; }; - struct investInProject_output + struct GetAuction_input { - + id auctionId; }; - struct claimToken_input + struct GetAuction_output { - uint64 amount; - uint32 indexOfFundraising; + AuctionData auction; }; - struct claimToken_output + struct GetAuctionParticipant_input { - uint64 claimedAmount; + id auctionId; + id participant; }; - struct upgradeTier_input + struct GetAuctionParticipant_output { - uint32 newTierLevel; + AuctionParticipantData participantData; + uint8 found; }; - struct upgradeTier_output + struct CreateAuction_locals { + AuctionData auction; + AuctionLotEntry lotItem; + AuctionLotEntry firstLotItem; + EAuctionType auctionType; + EAuctionVisibility visibility; + sint64 requiredFee; + uint64 totalEscrowQuantity; + uint64 lotItemIndex; + uint64 lotItemCount; + uint64 rollbackLotItemIndex; + uint64 allowedWalletCount; + sint64 possessedShares; + sint64 transferredShares; + uint64 allowedWalletIndex; + }; + struct PlaceBid_locals + { + AuctionData auction; + AuctionParticipantData participantData; + AuctionParticipantData previousHighestBidderData; + AuctionParticipantKey participantKey; + AuctionParticipantKey highestBidderKey; + uint64 elapsedSeconds; + uint64 requiredEscrow; + uint64 previousEscrow; + uint64 effectiveQuantity; + DateAndTime currentDate; + bool participantExists; + bool highestBidderExists; }; struct TransferShareManagementRights_input @@ -368,1281 +326,594 @@ struct NOST : public ContractBase sint64 transferredNumberOfShares; }; - struct getStats_input - { - - }; - - struct getStats_output - { - uint64 epochRevenue, totalPoolWeight; - uint32 numberOfRegister, numberOfCreatedProject, numberOfFundraising; - }; - - struct getTierLevelByUser_input + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() { - id userId; - }; - - struct getTierLevelByUser_output - { - uint8 tierLevel; - }; + REGISTER_USER_PROCEDURE(CreateAuction, 1); + REGISTER_USER_PROCEDURE(PlaceBid, 2); + REGISTER_USER_FUNCTION(GetAuction, 1); + REGISTER_USER_FUNCTION(GetAuctionParticipant, 2); - struct getUserVoteStatus_input - { - id userId; - }; + REGISTER_USER_PROCEDURE(TransferShareManagementRights, 1); + } - struct getUserVoteStatus_output - { - uint32 numberOfVotedProjects; - Array projectIndexList; - }; + INITIALIZE() {} - struct checkTokenCreatability_input + PRE_ACQUIRE_SHARES() { - uint64 tokenName; - }; + output.requestedFee = 0; + output.allowTransfer = true; + } - struct checkTokenCreatability_output + BEGIN_EPOCH() { - bit result; // result = 1 is the token already issued by SC - }; + // TODO: Change to valid epoch + if (qpi.epoch() == 220) + { + // Initialize + } + } - struct getNumberOfInvestedProjects_input + END_EPOCH() { - id userId; - }; + state.mut().auctionList.cleanupIfNeeded(); + state.mut().participants.cleanupIfNeeded(); + } - struct getNumberOfInvestedProjects_output + PUBLIC_PROCEDURE_WITH_LOCALS(CreateAuction) { - uint32 numberOfInvestedProjects; - }; + output.errorCode = static_cast(EAuctionError::InvalidInput); -protected: - - struct registerInTier_locals - { - uint64 tierStakedAmount; - uint32 poolWeight; - }; + locals.auctionType = static_cast(input.auctionType); + locals.visibility = static_cast(input.auctionVisibility); - PUBLIC_PROCEDURE_WITH_LOCALS(registerInTier) - { - if (state.get().users.contains(qpi.invocator())) + if (!isSupportedAuctionType(locals.auctionType)) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = static_cast(EAuctionError::InvalidAuctionType); + return; } - if (input.tierLevel < 1 || input.tierLevel > 5) + + if (!isSupportedAuctionVisibility(locals.visibility)) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = static_cast(EAuctionError::InvalidVisibility); + 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 (state.get().auctionList.population() >= state.get().auctionList.capacity()) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = static_cast(EAuctionError::StorageFull); + return; } - else + locals.totalEscrowQuantity = 0; + locals.lotItemCount = 0; + for (locals.lotItemIndex = 0; locals.lotItemIndex < NOST_AUCTION_LOT_ITEM_NUM; ++locals.lotItemIndex) { - state.mut().users.set(qpi.invocator(), input.tierLevel); - state.mut().numberOfRegister++; - if (qpi.invocationReward() > (sint64)locals.tierStakedAmount) + locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); + if (isZeroAsset(locals.lotItem.asset)) { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.tierStakedAmount); + if (locals.lotItem.quantity != 0) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + continue; } - state.mut().totalPoolWeight += locals.poolWeight; - output.tierLevel = input.tierLevel; - } - } - - struct logoutFromTier_locals - { - uint64 earnedAmount; - uint32 elementIndex; - uint8 tierLevel; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(logoutFromTier) - { - if (state.get().users.contains(qpi.invocator()) == 0) - { - return ; + if (locals.lotItem.quantity <= 0) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + if (locals.lotItemCount == 0) + { + locals.firstLotItem = locals.lotItem; + } + locals.lotItemCount = sadd(locals.lotItemCount, 1ULL); + locals.totalEscrowQuantity = sadd(locals.totalEscrowQuantity, static_cast(locals.lotItem.quantity)); } - state.get().users.get(qpi.invocator(), locals.tierLevel); - switch (locals.tierLevel) + if (locals.lotItemCount == 0 || input.durationDays == 0) { - 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; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; } - - state.mut().users.removeByKey(qpi.invocator()); - state.mut().numberOfRegister -= 1; - output.result = 1; - } - - struct createProject_locals - { - projectInfo newProject; - uint32 elementIndex, startDate, endDate, curDate; - uint8 tierLevel; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(createProject) - { - 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); - - if(locals.curDate > locals.startDate || locals.startDate >= locals.endDate || checkValidNostDateTime(locals.startDate) == 0 || checkValidNostDateTime(locals.endDate) == 0) + if (locals.auctionType == EAuctionType::Batch && + (locals.lotItemCount != 1 || input.minimumPurchaseQuantity == 0 || input.minimumPurchaseQuantity > locals.totalEscrowQuantity)) { - output.indexOfProject = NOSTROMO_MAX_NUMBER_PROJECT; if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } return; } - - if (state.get().tokens.contains(input.tokenName)) + if (locals.auctionType == EAuctionType::Standard && input.minimumPurchaseQuantity > 1) { - output.indexOfProject = NOSTROMO_MAX_NUMBER_PROJECT; if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + return; } - - if (state.get().users.get(qpi.invocator(), locals.tierLevel) && (locals.tierLevel == 4 || locals.tierLevel == 5)) + if (locals.auctionType == EAuctionType::Standard && input.minimumBidIncrementPerUnit == 0) { - if (qpi.invocationReward() < NOSTROMO_CREATE_PROJECT_FEE) + if (qpi.invocationReward() > 0) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.indexOfProject = NOSTROMO_MAX_NUMBER_PROJECT; - return ; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - if (qpi.invocationReward() > NOSTROMO_CREATE_PROJECT_FEE) + return; + } + locals.allowedWalletCount = 0; + for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < NOST_AUCTION_ALLOWED_WALLET_NUM; ++locals.allowedWalletIndex) + { + if (!isZero(input.allowedBidderWallets.get(locals.allowedWalletIndex))) { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - NOSTROMO_CREATE_PROJECT_FEE); + locals.allowedWalletCount = sadd(locals.allowedWalletCount, 1ULL); } - state.mut().epochRevenue += NOSTROMO_CREATE_PROJECT_FEE; - - 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; - - output.indexOfProject = state.get().numberOfCreatedProject; - state.mut().projects.set(state.get().numberOfCreatedProject, locals.newProject); - state.mut().numberOfCreatedProject++; - state.mut().tokens.add(input.tokenName); } - else + if (locals.visibility == EAuctionVisibility::Private && isZeroAsset(input.requiredAccessAsset) && locals.allowedWalletCount == 0) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.indexOfProject = NOSTROMO_MAX_NUMBER_PROJECT; + return; } - } - - struct voteInProject_locals - { - projectInfo votedProject; - Array votedList; - uint32 elementIndex, curDate, numberOfVotedProject, i; - bit flag; - }; - PUBLIC_PROCEDURE_WITH_LOCALS(voteInProject) - { - if (input.indexOfProject >= state.get().numberOfCreatedProject) - { - return ; - } - if (state.get().users.contains(qpi.invocator()) == 0) + locals.requiredFee = 0; + if (locals.visibility == EAuctionVisibility::Private) { - return ; + locals.requiredFee = NOST_PRIVATE_AUCTION_FEE; } - state.get().numberOfVotedProject.get(qpi.invocator(), locals.numberOfVotedProject); - if (locals.numberOfVotedProject == NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST) + if (qpi.invocationReward() < locals.requiredFee) { - return ; - } - state.get().voteStatus.get(qpi.invocator(), locals.votedList); - for (locals.i = 0; locals.i < locals.numberOfVotedProject; locals.i++) - { - if (locals.votedList.get(locals.i) == input.indexOfProject) + if (qpi.invocationReward() > 0) { - return ; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + output.errorCode = static_cast(EAuctionError::InsufficientFunds); + 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) + + for (locals.lotItemIndex = 0; locals.lotItemIndex < NOST_AUCTION_LOT_ITEM_NUM; ++locals.lotItemIndex) { - locals.votedProject = state.get().projects.get(input.indexOfProject); - if (input.decision) + locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); + if (isZeroAsset(locals.lotItem.asset) || locals.lotItem.quantity <= 0) { - locals.votedProject.numberOfYes++; + continue; } - else + 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) { - locals.votedProject.numberOfNo++; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::InsufficientAssetBalance); + 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; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(createFundraising) - { - 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); - - 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) + for (locals.lotItemIndex = 0; locals.lotItemIndex < NOST_AUCTION_LOT_ITEM_NUM; ++locals.lotItemIndex) { - if (qpi.invocationReward() > 0) + locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); + if (isZeroAsset(locals.lotItem.asset) || locals.lotItem.quantity <= 0) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + continue; + } + + locals.transferredShares = qpi.transferShareOwnershipAndPossession(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, + qpi.invocator(), qpi.invocator(), locals.lotItem.quantity, SELF); + if (locals.transferredShares < locals.lotItem.quantity) + { + if (locals.transferredShares > 0) + { + qpi.transferShareOwnershipAndPossession(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, SELF, SELF, + locals.transferredShares, qpi.invocator()); + } + 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()); + } + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::InsufficientAssetBalance); + return; } - 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) + + locals.auction.auctionId = id::randomValue(); + locals.auction.quantityForSale = (locals.auctionType == EAuctionType::Standard) ? 1ULL : locals.totalEscrowQuantity; + locals.auction.allocatedQuantity = 0; + locals.auction.minimumPurchaseQuantity = (locals.auctionType == EAuctionType::Standard) ? 1ULL : input.minimumPurchaseQuantity; + locals.auction.initialPricePerUnit = input.initialPricePerUnit; + locals.auction.salePricePerUnit = input.salePricePerUnit; + locals.auction.minimumBidIncrement = input.minimumBidIncrementPerUnit; + locals.auction.buyNowPricePerUnit = input.buyNowPricePerUnit; + locals.auction.highestBidPerUnit = 0; + locals.auction.highestBidQuantity = 0; + locals.auction.highestBidAmount = 0; + locals.auction.auctionDurationSeconds = smul(static_cast(input.durationDays), NOST_SECONDS_PER_DAY); + locals.auction.createdAt = qpi.now(); + locals.auction.lastBidAt = locals.auction.createdAt; + locals.auction.sellerDecisionDeadline = DateAndTime(); + locals.auction.settledAt = DateAndTime(); + locals.auction.bidderCount = 0; + locals.auction.seller = qpi.invocator(); + locals.auction.highestBidder = NULL_ID; + locals.auction.assetForSale = locals.firstLotItem.asset; + locals.auction.requiredAccessAsset = input.requiredAccessAsset; + locals.auction.auctionLotItems = input.auctionLotItems; + for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < NOST_AUCTION_ALLOWED_WALLET_NUM; ++locals.allowedWalletIndex) { - if (qpi.invocationReward() > 0) + if (!isZero(input.allowedBidderWallets.get(locals.allowedWalletIndex))) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + locals.auction.allowedBidderWallets.add(input.allowedBidderWallets.get(locals.allowedWalletIndex)); } - return ; } + locals.auction.metadataIpfsCid = input.metadataIpfsCid; + locals.auction.type = locals.auctionType; + locals.auction.visibility = locals.visibility; + locals.auction.status = EAuctionStatus::Active; - if (input.stepOfVesting == 0 || input.stepOfVesting > 12 || input.TGE > 50 || input.threshold > 50 || input.indexOfProject >= state.get().numberOfCreatedProject) + if (state.mut().auctionList.set(locals.auction.auctionId, locals.auction) == NULL_INDEX) { + for (locals.rollbackLotItemIndex = 0; locals.rollbackLotItemIndex < NOST_AUCTION_LOT_ITEM_NUM; ++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()); + } if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = static_cast(EAuctionError::StorageFull); + return; + } + + if (static_cast(qpi.invocationReward()) > locals.requiredFee) + { + qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - locals.requiredFee); } + output.auctionId = locals.auction.auctionId; + output.errorCode = static_cast(EAuctionError::Success); + } + + PUBLIC_PROCEDURE_WITH_LOCALS(PlaceBid) + { + output.errorCode = static_cast(EAuctionError::InvalidInput); - if (state.get().projects.get(input.indexOfProject).creator != qpi.invocator()) + if (!state.get().auctionList.get(input.auctionId, locals.auction)) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = static_cast(EAuctionError::AuctionNotFound); + return; } - - if (input.soldAmount > state.get().projects.get(input.indexOfProject).supplyOfToken) + if (locals.auction.status != EAuctionStatus::Active) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = static_cast(EAuctionError::AuctionClosed); + 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) + if (locals.auction.seller == qpi.invocator()) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = static_cast(EAuctionError::Forbidden); + return; } - if (input.tokenPrice * input.soldAmount < input.requiredFunds + div(input.requiredFunds * input.threshold, 100ULL)) + locals.currentDate = qpi.now(); + diffDateInSecond(locals.auction.createdAt, locals.currentDate, locals.elapsedSeconds); + if (locals.elapsedSeconds >= locals.auction.auctionDurationSeconds) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = static_cast(EAuctionError::AuctionClosed); + return; } - if (qpi.invocationReward() < NOSTROMO_QX_TOKEN_ISSUANCE_FEE) + if (locals.auction.visibility == EAuctionVisibility::Private && + qpi.numberOfShares(locals.auction.requiredAccessAsset, AssetOwnershipSelect::byOwner(qpi.invocator()), + AssetPossessionSelect::byPossessor(qpi.invocator())) <= 0) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = static_cast(EAuctionError::PrivateAuctionAccessDenied); + return; } - if (qpi.invocationReward() > NOSTROMO_QX_TOKEN_ISSUANCE_FEE) + locals.effectiveQuantity = input.quantity; + if (locals.auction.type == EAuctionType::Standard) { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - NOSTROMO_QX_TOKEN_ISSUANCE_FEE); + locals.effectiveQuantity = locals.auction.quantityForSale; } - - 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++; - } - - 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) - { - if (input.indexOfFundraising >= state.get().numberOfFundraising || qpi.invocationReward() == 0) + if (locals.effectiveQuantity == 0 || locals.effectiveQuantity < locals.auction.minimumPurchaseQuantity || input.pricePerUnit == 0) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + return; } - locals.maxCap = state.get().fundaraisings.get(input.indexOfFundraising).requiredFunds + div(state.get().fundaraisings.get(input.indexOfFundraising).requiredFunds * state.get().fundaraisings.get(input.indexOfFundraising).threshold, 100ULL); - locals.minCap = state.get().fundaraisings.get(input.indexOfFundraising).requiredFunds - div(state.get().fundaraisings.get(input.indexOfFundraising).requiredFunds * state.get().fundaraisings.get(input.indexOfFundraising).threshold, 100ULL); - if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects) && locals.numberOfInvestedProjects >= NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST) + if (locals.auction.type == EAuctionType::Batch) { - if (qpi.invocationReward() > 0) + if (input.pricePerUnit < locals.auction.salePricePerUnit) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::BidTooLow); + return; } - return ; } - - 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) + else { - if (state.get().users.contains(qpi.invocator()) == 0) + if (locals.auction.highestBidPerUnit == 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 (input.pricePerUnit < locals.auction.initialPricePerUnit) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; + output.errorCode = static_cast(EAuctionError::BidTooLow); + 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 (input.pricePerUnit < sadd(locals.auction.highestBidPerUnit, locals.auction.minimumBidIncrement)) { 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); + output.errorCode = static_cast(EAuctionError::BidTooLow); + return; } } } - 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++) - { - 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); - } - } - } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).thirdPhaseStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).thirdPhaseEndDate) - { - if (locals.tmpFundraising.raisedFunds + qpi.invocationReward() > locals.maxCap) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; - } - state.get().investors.get(qpi.invocator(), state.mut().tmpInvestedList); - state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects); - - for (locals.i = 0; locals.i < locals.numberOfInvestedProjects; locals.i++) - { - 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) - { - locals.tmpInvestData.investedAmount = qpi.invocationReward() + locals.userInvestedAmount; - state.mut().tmpInvestedList.set(locals.i, locals.tmpInvestData); - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - } - else - { - locals.tmpInvestData.investedAmount = qpi.invocationReward(); - - state.mut().tmpInvestedList.set(locals.numberOfInvestedProjects, locals.tmpInvestData); - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects)) - { - state.mut().numberOfInvestedProjects.set(qpi.invocator(), locals.numberOfInvestedProjects + 1); - } - else - { - state.mut().numberOfInvestedProjects.set(qpi.invocator(), 1); - } - } - locals.tmpFundraising.raisedFunds += qpi.invocationReward(); - } - else + locals.requiredEscrow = smul(locals.effectiveQuantity, input.pricePerUnit); + if (static_cast(qpi.invocationReward()) < locals.requiredEscrow) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - return ; - } - if (locals.minCap <= locals.tmpFundraising.raisedFunds && locals.tmpFundraising.isCreatedToken == 0) - { - locals.input.assetName = state.get().projects.get(locals.tmpFundraising.indexOfProject).tokenName; - locals.input.numberOfDecimalPlaces = 0; - locals.input.numberOfShares = state.get().projects.get(locals.tmpFundraising.indexOfProject).supplyOfToken; - locals.input.unitOfMeasurement = 0; - - INVOKE_OTHER_CONTRACT_PROCEDURE(QX, IssueAsset, locals.input, locals.output, NOSTROMO_QX_TOKEN_ISSUANCE_FEE); - - if (locals.output.issuedNumberOfShares == state.get().projects.get(locals.tmpFundraising.indexOfProject).supplyOfToken) - { - locals.tmpFundraising.isCreatedToken = 1; - - locals.TransferShareManagementRightsInput.asset.assetName = state.get().projects.get(locals.tmpFundraising.indexOfProject).tokenName; - locals.TransferShareManagementRightsInput.asset.issuer = SELF; - locals.TransferShareManagementRightsInput.newManagingContractIndex = SELF_INDEX; - locals.TransferShareManagementRightsInput.numberOfShares = state.get().projects.get(locals.tmpFundraising.indexOfProject).supplyOfToken; - - INVOKE_OTHER_CONTRACT_PROCEDURE(QX, TransferShareManagementRights, locals.TransferShareManagementRightsInput, locals.TransferShareManagementRightsOutput, 0); - - qpi.transferShareOwnershipAndPossession(state.get().projects.get(locals.tmpFundraising.indexOfProject).tokenName, SELF, SELF, SELF, state.get().projects.get(locals.tmpFundraising.indexOfProject).supplyOfToken - locals.tmpFundraising.soldAmount, state.get().projects.get(locals.tmpFundraising.indexOfProject).creator); - } - } - - state.mut().fundaraisings.set(input.indexOfFundraising, locals.tmpFundraising); - - } - - struct claimToken_locals - { - investInfo tmpInvestData; - uint64 maxClaimAmount, investedAmount, dayA, dayB, start_cur_diffSecond, cur_end_diffSecond, claimedAmount; - uint32 curDate, tmpDate, numberOfInvestedProjects; - sint32 i, j; - uint8 curVestingStep, vestingPercent; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(claimToken) - { - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); - - if (input.indexOfFundraising >= state.get().numberOfFundraising) - { - return ; - } - - state.get().investors.get(qpi.invocator(), state.mut().tmpInvestedList); - if (state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects) == 0) - { - return ; + output.errorCode = static_cast(EAuctionError::InsufficientFunds); + return; } - for (locals.i = 0; locals.i < (sint32)locals.numberOfInvestedProjects; locals.i++) + locals.participantKey = {input.auctionId, qpi.invocator()}; + locals.participantExists = state.get().participants.get(locals.participantKey, locals.participantData); + locals.previousEscrow = 0; + if (locals.participantExists) { - if (state.get().tmpInvestedList.get(locals.i).indexOfFundraising == input.indexOfFundraising) - { - locals.investedAmount = state.get().tmpInvestedList.get(locals.i).investedAmount; - locals.claimedAmount = state.get().tmpInvestedList.get(locals.i).claimedAmount; - locals.tmpInvestData = state.get().tmpInvestedList.get(locals.i); - break; - } + locals.previousEscrow = locals.participantData.escrowedAmount; } - if (locals.i == locals.numberOfInvestedProjects) - { - return ; - } + locals.participantData.escrowedAmount = locals.requiredEscrow; + locals.participantData.requestedQuantity = locals.effectiveQuantity; + locals.participantData.allocatedQuantity = 0; + locals.participantData.pricePerUnit = input.pricePerUnit; + locals.participantData.lastBidTime = locals.currentDate; + locals.participantData.participant = qpi.invocator(); + locals.participantData.isHighestBidder = 0; + locals.participantData.isWinningBid = 0; - if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).listingStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate) - { - locals.maxClaimAmount = div(div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice) * state.get().fundaraisings.get(input.indexOfFundraising).TGE, 100ULL); - } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate) - { - locals.tmpDate = state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate; - diffDateInSecond(locals.tmpDate, locals.curDate, locals.j, locals.dayA, locals.dayB, locals.start_cur_diffSecond); - locals.tmpDate = state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate; - diffDateInSecond(locals.curDate, locals.tmpDate, locals.j, locals.dayA, locals.dayB, locals.cur_end_diffSecond); - - locals.curVestingStep = (uint8)div(locals.start_cur_diffSecond, div(locals.start_cur_diffSecond + locals.cur_end_diffSecond, state.get().fundaraisings.get(input.indexOfFundraising).stepOfVesting * 1ULL)) + 1; - locals.vestingPercent = (uint8)div(100ULL - state.get().fundaraisings.get(input.indexOfFundraising).TGE, state.get().fundaraisings.get(input.indexOfFundraising).stepOfVesting * 1ULL) * locals.curVestingStep; - locals.maxClaimAmount = div(div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice) * (state.get().fundaraisings.get(input.indexOfFundraising).TGE + locals.vestingPercent), 100ULL); - } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate) + if (!locals.participantExists) { - locals.maxClaimAmount = div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice); + locals.auction.bidderCount = sadd(locals.auction.bidderCount, 1U); } - if (input.amount + locals.claimedAmount > locals.maxClaimAmount) + if (locals.auction.type == EAuctionType::Standard) { - return ; - } - else - { - 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) - { - 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); - } - state.mut().investors.set(qpi.invocator(), state.get().tmpInvestedList); - state.get().numberOfInvestedProjects.get(qpi.invocator(), locals.numberOfInvestedProjects); - if (locals.numberOfInvestedProjects == 0) + locals.highestBidderExists = false; + if (!isZero(locals.auction.highestBidder)) { - state.mut().investors.removeByKey(qpi.invocator()); - state.mut().numberOfInvestedProjects.removeByKey(qpi.invocator()); + locals.highestBidderKey = {input.auctionId, locals.auction.highestBidder}; + locals.highestBidderExists = state.get().participants.get(locals.highestBidderKey, locals.previousHighestBidderData); } - output.claimedAmount = input.amount; - } - } - - struct upgradeTier_locals - { - uint64 deltaAmount; - uint32 i, deltaPoolWeight; - uint8 currentTierLevel; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(upgradeTier) - { - if (state.get().users.contains(qpi.invocator()) == 0) - { - if (qpi.invocationReward() > 0) + if (locals.highestBidderExists && locals.previousHighestBidderData.participant != qpi.invocator()) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + qpi.transfer(locals.previousHighestBidderData.participant, locals.previousHighestBidderData.escrowedAmount); + output.refundedAmount = sadd(output.refundedAmount, locals.previousHighestBidderData.escrowedAmount); + locals.previousHighestBidderData.escrowedAmount = 0; + locals.previousHighestBidderData.isHighestBidder = 0; + locals.previousHighestBidderData.isWinningBid = 0; + state.mut().participants.replace(locals.highestBidderKey, locals.previousHighestBidderData); } - 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) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return ; + locals.participantData.isHighestBidder = 1; + locals.participantData.isWinningBid = 1; + locals.auction.highestBidder = qpi.invocator(); + locals.auction.highestBidPerUnit = input.pricePerUnit; + locals.auction.highestBidQuantity = locals.effectiveQuantity; + locals.auction.highestBidAmount = locals.requiredEscrow; } else { - state.mut().users.set(qpi.invocator(), input.newTierLevel); - if (qpi.invocationReward() > (sint64)locals.deltaAmount) + if (input.pricePerUnit > locals.auction.highestBidPerUnit) { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.deltaAmount); + locals.auction.highestBidder = qpi.invocator(); + locals.auction.highestBidPerUnit = input.pricePerUnit; + locals.auction.highestBidQuantity = locals.effectiveQuantity; + locals.auction.highestBidAmount = locals.requiredEscrow; + locals.participantData.isHighestBidder = 1; } - state.mut().totalPoolWeight += locals.deltaPoolWeight; } - } - PUBLIC_PROCEDURE(TransferShareManagementRights) - { - if (qpi.invocationReward() < state.get().transferRightsFee) + locals.auction.lastBidAt = locals.currentDate; + if ((locals.auction.auctionDurationSeconds - locals.elapsedSeconds) <= NOST_AUCTION_EXTENSION_SECONDS) + { + locals.auction.auctionDurationSeconds = sadd(locals.auction.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); + } + if (locals.auction.buyNowPricePerUnit > 0 && input.pricePerUnit >= locals.auction.buyNowPricePerUnit) { - return ; + locals.auction.status = EAuctionStatus::Finalized; + locals.auction.settledAt = locals.currentDate; + locals.participantData.isWinningBid = 1; + locals.participantData.isHighestBidder = 1; } - if (qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer,qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.numberOfShares) + if (state.mut().participants.set(locals.participantKey, locals.participantData) == NULL_INDEX) { - // not enough shares available - output.transferredNumberOfShares = 0; if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + output.errorCode = static_cast(EAuctionError::StorageFull); + return; } - else - { - if (qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, - input.newManagingContractIndex, input.newManagingContractIndex, state.get().transferRightsFee) < 0) - { - // error - output.transferredNumberOfShares = 0; - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - } - else - { - // success - output.transferredNumberOfShares = input.numberOfShares; - if (qpi.invocationReward() > state.get().transferRightsFee) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.get().transferRightsFee); - } - } - } - } - - PUBLIC_FUNCTION(getStats) - { - 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; - } + state.mut().auctionList.replace(input.auctionId, locals.auction); - PUBLIC_FUNCTION(getTierLevelByUser) - { - state.get().users.get(input.userId, output.tierLevel); - } - - PUBLIC_FUNCTION(getUserVoteStatus) - { - state.get().numberOfVotedProject.get(input.userId, output.numberOfVotedProjects); - state.get().voteStatus.get(input.userId, output.projectIndexList); - } - - PUBLIC_FUNCTION(checkTokenCreatability) - { - output.result = state.get().tokens.contains(input.tokenName); - } - - PUBLIC_FUNCTION(getNumberOfInvestedProjects) - { - state.get().numberOfInvestedProjects.get(input.userId, output.numberOfInvestedProjects); - } - -public: - struct getProjectByIndex_input - { - uint32 indexOfProject; - }; - - struct getProjectByIndex_output - { - projectInfo project; - }; - - PUBLIC_FUNCTION(getProjectByIndex) - { - output.project = state.get().projects.get(input.indexOfProject); - } - - struct getFundarasingByIndex_input - { - uint32 indexOfFundarasing; - }; - - struct getFundarasingByIndex_output - { - fundaraisingInfo fundarasing; - }; - - PUBLIC_FUNCTION(getFundarasingByIndex) - { - output.fundarasing = state.get().fundaraisings.get(input.indexOfFundarasing); - } - - struct getProjectIndexListByCreator_input - { - id creator; - }; - - struct getProjectIndexListByCreator_output - { - Array indexListForProjects; - }; - - struct getProjectIndexListByCreator_locals - { - uint32 i, countOfProject; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(getProjectIndexListByCreator) - { - for (locals.i = 0; locals.i < state.get().numberOfCreatedProject; locals.i++) + if (locals.previousEscrow > 0) { - if (state.get().projects.get(locals.i).creator == input.creator) - { - output.indexListForProjects.set(locals.countOfProject++, locals.i); - } + qpi.transfer(qpi.invocator(), locals.previousEscrow); + output.refundedAmount = sadd(output.refundedAmount, locals.previousEscrow); } - for (locals.i = locals.countOfProject; locals.i < NOSTROMO_MAX_NUMBER_OF_PROJECT_USER_INVEST; locals.i++) + if (static_cast(qpi.invocationReward()) > locals.requiredEscrow) { - output.indexListForProjects.set(locals.i, NOSTROMO_MAX_NUMBER_PROJECT); + qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - locals.requiredEscrow); + output.refundedAmount = sadd(output.refundedAmount, static_cast(qpi.invocationReward()) - locals.requiredEscrow); } - } - struct getInfoUserInvested_input - { - id investorId; - }; - - struct getInfoUserInvested_output - { - Array listUserInvested; - }; - - struct getInfoUserInvested_locals - { - uint32 i, countOfProject; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(getInfoUserInvested) - { - state.get().investors.get(input.investorId, output.listUserInvested); + output.escrowedAmount = locals.requiredEscrow; + output.errorCode = static_cast(EAuctionError::Success); } - struct getMaxClaimAmount_input - { - id investorId; - uint32 indexOfFundraising; - }; - - struct getMaxClaimAmount_output - { - uint64 amount; - }; + PUBLIC_FUNCTION(GetAuction) { state.get().auctionList.get(input.auctionId, output.auction); } - struct getMaxClaimAmount_locals + PUBLIC_FUNCTION(GetAuctionParticipant) { - 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; - }; + output.found = state.get().participants.get({input.auctionId, input.participant}, output.participantData) ? 1 : 0; + } - PUBLIC_FUNCTION_WITH_LOCALS(getMaxClaimAmount) + PUBLIC_PROCEDURE(TransferShareManagementRights) { - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); - - if (input.indexOfFundraising >= state.get().numberOfFundraising) + if (qpi.invocationReward() > 0) { - return ; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - state.get().investors.get(input.investorId, locals.tmpInvestedList); - if (state.get().numberOfInvestedProjects.get(input.investorId, locals.numberOfInvestedProjects) == 0) + if (input.numberOfShares <= 0 || input.asset.assetName == 0 || input.newManagingContractIndex == 0) { - return ; + output.transferredNumberOfShares = 0; + return; } - for (locals.i = 0; locals.i < (sint32)locals.numberOfInvestedProjects; locals.i++) + if (qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < + input.numberOfShares) { - 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; - } + output.transferredNumberOfShares = 0; + return; } - if (locals.i == locals.numberOfInvestedProjects) + if (qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, input.newManagingContractIndex, + input.newManagingContractIndex, 0) < 0) { - return ; + // error + output.transferredNumberOfShares = 0; + return; } - if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).listingStartDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate) - { - output.amount = div(div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice) * state.get().fundaraisings.get(input.indexOfFundraising).TGE, 100ULL); - } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate && locals.curDate < state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate) - { - locals.tmpDate = state.get().fundaraisings.get(input.indexOfFundraising).cliffEndDate; - diffDateInSecond(locals.tmpDate, locals.curDate, locals.j, locals.dayA, locals.dayB, locals.start_cur_diffSecond); - locals.tmpDate = state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate; - diffDateInSecond(locals.curDate, locals.tmpDate, locals.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); - } - else if (locals.curDate >= state.get().fundaraisings.get(input.indexOfFundraising).vestingEndDate) - { - output.amount = div(locals.investedAmount, state.get().fundaraisings.get(input.indexOfFundraising).tokenPrice); - } + // success + output.transferredNumberOfShares = input.numberOfShares; } - REGISTER_USER_FUNCTIONS_AND_PROCEDURES() +protected: + static bool isSupportedAuctionType(EAuctionType auctionType) { - 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 auctionType == EAuctionType::Batch || auctionType == EAuctionType::Standard; } - INITIALIZE() + static bool isSupportedAuctionVisibility(EAuctionVisibility visibility) { - 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 visibility == EAuctionVisibility::Public || visibility == EAuctionVisibility::Private; } - struct END_EPOCH_locals - { - fundaraisingInfo tmpFundraising; - investInfo tmpInvest; - Array votedList; - Array clearedVotedList; - id userId; - sint64 idx; - uint32 numberOfVotedProject, clearedNumberOfVotedProject, i, j, curDate, indexOfProject, numberOfInvestedProjects, tierLevel; - }; + static bool isZeroAsset(const Asset& asset) { return asset.assetName == 0 && isZero(asset.issuer); } - END_EPOCH_WITH_LOCALS() + /** + * @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) { - packNostromoDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); - - locals.idx = state.get().investors.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) + if (a < b) { - locals.userId = state.get().investors.key(locals.idx); - state.get().investors.get(locals.userId, state.mut().tmpInvestedList); - state.get().numberOfInvestedProjects.get(locals.userId, locals.numberOfInvestedProjects); - - for (locals.i = 0; locals.i < locals.numberOfInvestedProjects; locals.i++) - { - if (state.get().fundaraisings.get(locals.i).thirdPhaseEndDate < locals.curDate && state.get().fundaraisings.get(locals.i).isCreatedToken == 0 && state.get().fundaraisings.get(locals.i).raisedFunds != 0) - { - qpi.transfer(locals.userId, state.get().tmpInvestedList.get(locals.i).investedAmount); - state.mut().tmpInvestedList.set(locals.i, state.get().tmpInvestedList.get(--locals.numberOfInvestedProjects)); - } - } - if (locals.numberOfInvestedProjects == 0) - { - state.mut().investors.removeByKey(locals.userId); - state.mut().numberOfInvestedProjects.removeByKey(locals.userId); - } - else - { - state.mut().investors.set(locals.userId, state.get().tmpInvestedList); - state.mut().numberOfInvestedProjects.set(locals.userId, locals.numberOfInvestedProjects); - } - locals.idx = state.get().investors.nextElementIndex(locals.idx); + return -1; } - - for (locals.i = 0; locals.i < state.get().numberOfFundraising; locals.i++) + if (a > b) { - 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); - - 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)); - - 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); - - locals.tmpFundraising.raisedFunds = 0; - state.mut().fundaraisings.set(locals.i, locals.tmpFundraising); - } + return 1; } + return 0; + } - 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; - - locals.idx = state.get().users.nextElementIndex(NULL_INDEX); - while (locals.idx != NULL_INDEX) + /** + * @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) { - locals.userId = state.get().users.key(locals.idx); - locals.tierLevel = state.get().users.value(locals.idx); - - 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); - - 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); - } - } - - locals.idx = state.get().users.nextElementIndex(locals.idx); + res = 0; + return; } - - 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(); } + res = div(a.durationMicrosec(b), 1000000ULL); } - - PRE_ACQUIRE_SHARES() - { - output.allowTransfer = true; - } }; From d7a37155cef9dd69252f549d2486d56e4faa73ae Mon Sep 17 00:00:00 2001 From: N-010 Date: Tue, 14 Apr 2026 22:06:05 +0300 Subject: [PATCH 02/59] Refactor `CreateAuction` procedure in `Nostromo` contract: modularize auction parameter resolution, lot asset management, and data validation into reusable private functions. --- src/contracts/Nostromo.h | 601 +++++++++++++++++++++++++++++---------- 1 file changed, 458 insertions(+), 143 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index c182420ce..e3f11e463 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -281,24 +281,206 @@ struct NOST : public ContractBase uint8 found; }; - struct CreateAuction_locals + struct ResolveBatchAuctionCreateParams_input { - AuctionData auction; - AuctionLotEntry lotItem; + uint64 lotItemCount; + uint64 totalEscrowQuantity; + uint64 minimumPurchaseQuantity; + }; + + struct ResolveBatchAuctionCreateParams_output + { + uint64 quantityForSale; + uint64 minimumPurchaseQuantity; + uint8 isValid; + }; + + struct ResolveBatchAuctionCreateParams_locals + { + }; + + struct ResolveStandardAuctionCreateParams_input + { + uint64 minimumPurchaseQuantity; + uint64 minimumBidIncrementPerUnit; + }; + + struct ResolveStandardAuctionCreateParams_output + { + uint64 quantityForSale; + uint64 minimumPurchaseQuantity; + uint8 isValid; + }; + + struct ResolveStandardAuctionCreateParams_locals + { + }; + + struct AnalyzeAuctionLot_input + { + Array auctionLotItems; + uint32 durationDays; + }; + + struct AnalyzeAuctionLot_output + { + uint64 totalEscrowQuantity; + uint64 lotItemCount; AuctionLotEntry firstLotItem; - EAuctionType auctionType; + uint8 isValid; + }; + + struct AnalyzeAuctionLot_locals + { + AuctionLotEntry lotItem; + uint64 lotItemIndex; + }; + + struct CountAllowedBidderWallets_input + { + Array allowedBidderWallets; + }; + + struct CountAllowedBidderWallets_output + { + uint64 allowedWalletCount; + }; + + struct CountAllowedBidderWallets_locals + { + uint64 allowedWalletIndex; + }; + + struct ValidatePrivateAuctionAccess_input + { EAuctionVisibility visibility; + Asset requiredAccessAsset; + uint64 allowedWalletCount; + }; + + struct ValidatePrivateAuctionAccess_output + { + uint8 isValid; + }; + + struct ValidatePrivateAuctionAccess_locals + { + }; + + struct GetCreateAuctionFee_input + { + EAuctionVisibility visibility; + }; + + struct GetCreateAuctionFee_output + { sint64 requiredFee; - uint64 totalEscrowQuantity; + }; + + struct GetCreateAuctionFee_locals + { + }; + + struct VerifyAuctionLotBalances_input + { + Array auctionLotItems; + }; + + struct VerifyAuctionLotBalances_output + { + uint8 hasEnoughBalance; + }; + + struct VerifyAuctionLotBalances_locals + { + AuctionLotEntry lotItem; uint64 lotItemIndex; - uint64 lotItemCount; - uint64 rollbackLotItemIndex; - uint64 allowedWalletCount; sint64 possessedShares; + }; + + struct EscrowAuctionLotAssets_input + { + Array auctionLotItems; + }; + + struct EscrowAuctionLotAssets_output + { + uint8 success; + }; + + struct EscrowAuctionLotAssets_locals + { + AuctionLotEntry lotItem; + uint64 lotItemIndex; + uint64 rollbackLotItemIndex; sint64 transferredShares; + }; + + struct RollbackAuctionLotAssets_input + { + Array auctionLotItems; + }; + + struct RollbackAuctionLotAssets_output + { + }; + + struct RollbackAuctionLotAssets_locals + { + AuctionLotEntry lotItem; + uint64 lotItemIndex; + }; + + struct BuildAuctionData_input + { + CreateAuction_input createInput; + AuctionLotEntry firstLotItem; + EAuctionType auctionType; + EAuctionVisibility visibility; + uint64 quantityForSale; + uint64 minimumPurchaseQuantity; + }; + + struct BuildAuctionData_output + { + AuctionData auction; + }; + + struct BuildAuctionData_locals + { uint64 allowedWalletIndex; }; + struct CreateAuction_locals + { + AuctionData auction; + AnalyzeAuctionLot_input analyzeAuctionLotInput; + AnalyzeAuctionLot_output analyzeAuctionLotOutput; + CountAllowedBidderWallets_input countAllowedBidderWalletsInput; + CountAllowedBidderWallets_output countAllowedBidderWalletsOutput; + ValidatePrivateAuctionAccess_input validatePrivateAuctionAccessInput; + ValidatePrivateAuctionAccess_output validatePrivateAuctionAccessOutput; + GetCreateAuctionFee_input getCreateAuctionFeeInput; + GetCreateAuctionFee_output getCreateAuctionFeeOutput; + VerifyAuctionLotBalances_input verifyAuctionLotBalancesInput; + VerifyAuctionLotBalances_output verifyAuctionLotBalancesOutput; + EscrowAuctionLotAssets_input escrowAuctionLotAssetsInput; + EscrowAuctionLotAssets_output escrowAuctionLotAssetsOutput; + RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; + RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; + BuildAuctionData_input buildAuctionDataInput; + BuildAuctionData_output buildAuctionDataOutput; + ResolveBatchAuctionCreateParams_input resolveBatchParamsInput; + ResolveBatchAuctionCreateParams_output resolveBatchParamsOutput; + ResolveStandardAuctionCreateParams_input resolveStandardParamsInput; + ResolveStandardAuctionCreateParams_output resolveStandardParamsOutput; + EAuctionType auctionType; + EAuctionVisibility visibility; + sint64 requiredFee; + uint64 resolvedQuantityForSale; + uint64 resolvedMinimumPurchaseQuantity; + }; + struct PlaceBid_locals { AuctionData auction; @@ -359,44 +541,49 @@ struct NOST : public ContractBase state.mut().participants.cleanupIfNeeded(); } - PUBLIC_PROCEDURE_WITH_LOCALS(CreateAuction) + PRIVATE_FUNCTION_WITH_LOCALS(ResolveBatchAuctionCreateParams) { - output.errorCode = static_cast(EAuctionError::InvalidInput); + output.quantityForSale = 0; + output.minimumPurchaseQuantity = 0; + output.isValid = 0; - locals.auctionType = static_cast(input.auctionType); - locals.visibility = static_cast(input.auctionVisibility); - - if (!isSupportedAuctionType(locals.auctionType)) + if (input.lotItemCount != 1 || input.minimumPurchaseQuantity == 0 || input.minimumPurchaseQuantity > input.totalEscrowQuantity) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = static_cast(EAuctionError::InvalidAuctionType); return; } - if (!isSupportedAuctionVisibility(locals.visibility)) + output.quantityForSale = input.totalEscrowQuantity; + output.minimumPurchaseQuantity = input.minimumPurchaseQuantity; + output.isValid = 1; + } + + PRIVATE_FUNCTION_WITH_LOCALS(ResolveStandardAuctionCreateParams) + { + output.quantityForSale = 0; + output.minimumPurchaseQuantity = 0; + output.isValid = 0; + + if (input.minimumPurchaseQuantity > 1 || input.minimumBidIncrementPerUnit == 0) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = static_cast(EAuctionError::InvalidVisibility); return; } - if (state.get().auctionList.population() >= state.get().auctionList.capacity()) + output.quantityForSale = 1; + output.minimumPurchaseQuantity = 1; + output.isValid = 1; + } + + PRIVATE_FUNCTION_WITH_LOCALS(AnalyzeAuctionLot) + { + output.totalEscrowQuantity = 0; + output.lotItemCount = 0; + output.isValid = 0; + + if (input.durationDays == 0) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = static_cast(EAuctionError::StorageFull); return; } - locals.totalEscrowQuantity = 0; - locals.lotItemCount = 0; + for (locals.lotItemIndex = 0; locals.lotItemIndex < NOST_AUCTION_LOT_ITEM_NUM; ++locals.lotItemIndex) { locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); @@ -404,94 +591,61 @@ struct NOST : public ContractBase { if (locals.lotItem.quantity != 0) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } return; } continue; } + if (locals.lotItem.quantity <= 0) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } return; } - if (locals.lotItemCount == 0) - { - locals.firstLotItem = locals.lotItem; - } - locals.lotItemCount = sadd(locals.lotItemCount, 1ULL); - locals.totalEscrowQuantity = sadd(locals.totalEscrowQuantity, static_cast(locals.lotItem.quantity)); - } - if (locals.lotItemCount == 0 || input.durationDays == 0) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - if (locals.auctionType == EAuctionType::Batch && - (locals.lotItemCount != 1 || input.minimumPurchaseQuantity == 0 || input.minimumPurchaseQuantity > locals.totalEscrowQuantity)) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - if (locals.auctionType == EAuctionType::Standard && input.minimumPurchaseQuantity > 1) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - if (locals.auctionType == EAuctionType::Standard && input.minimumBidIncrementPerUnit == 0) - { - if (qpi.invocationReward() > 0) + + if (output.lotItemCount == 0) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.firstLotItem = locals.lotItem; } - return; + + output.lotItemCount = sadd(output.lotItemCount, 1ULL); + output.totalEscrowQuantity = sadd(output.totalEscrowQuantity, static_cast(locals.lotItem.quantity)); } - locals.allowedWalletCount = 0; + + output.isValid = output.lotItemCount > 0 ? 1 : 0; + } + + PRIVATE_FUNCTION_WITH_LOCALS(CountAllowedBidderWallets) + { + output.allowedWalletCount = 0; for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < NOST_AUCTION_ALLOWED_WALLET_NUM; ++locals.allowedWalletIndex) { if (!isZero(input.allowedBidderWallets.get(locals.allowedWalletIndex))) { - locals.allowedWalletCount = sadd(locals.allowedWalletCount, 1ULL); + output.allowedWalletCount = sadd(output.allowedWalletCount, 1ULL); } } - if (locals.visibility == EAuctionVisibility::Private && isZeroAsset(input.requiredAccessAsset) && locals.allowedWalletCount == 0) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } + } - locals.requiredFee = 0; - if (locals.visibility == EAuctionVisibility::Private) + PRIVATE_FUNCTION_WITH_LOCALS(ValidatePrivateAuctionAccess) + { + output.isValid = 1; + if (input.visibility == EAuctionVisibility::Private && isZeroAsset(input.requiredAccessAsset) && input.allowedWalletCount == 0) { - locals.requiredFee = NOST_PRIVATE_AUCTION_FEE; + output.isValid = 0; } - if (qpi.invocationReward() < locals.requiredFee) + } + + PRIVATE_FUNCTION_WITH_LOCALS(GetCreateAuctionFee) + { + output.requiredFee = 0; + if (input.visibility == EAuctionVisibility::Private) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = static_cast(EAuctionError::InsufficientFunds); - return; + output.requiredFee = NOST_PRIVATE_AUCTION_FEE; } + } + PRIVATE_FUNCTION_WITH_LOCALS(VerifyAuctionLotBalances) + { + output.hasEnoughBalance = 1; for (locals.lotItemIndex = 0; locals.lotItemIndex < NOST_AUCTION_LOT_ITEM_NUM; ++locals.lotItemIndex) { locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); @@ -499,19 +653,34 @@ struct NOST : public ContractBase { continue; } + locals.possessedShares = qpi.numberOfPossessedShares(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, qpi.invocator(), - qpi.invocator(), SELF_INDEX, SELF_INDEX); + qpi.invocator(), SELF_INDEX, SELF_INDEX); if (locals.possessedShares < locals.lotItem.quantity) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = static_cast(EAuctionError::InsufficientAssetBalance); + output.hasEnoughBalance = 0; return; } } + } + PRIVATE_PROCEDURE_WITH_LOCALS(RollbackAuctionLotAssets) + { + for (locals.lotItemIndex = 0; locals.lotItemIndex < NOST_AUCTION_LOT_ITEM_NUM; ++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, + qpi.invocator()); + } + } + + PRIVATE_FUNCTION_WITH_LOCALS(EscrowAuctionLotAssets) + { + output.success = 1; for (locals.lotItemIndex = 0; locals.lotItemIndex < NOST_AUCTION_LOT_ITEM_NUM; ++locals.lotItemIndex) { locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); @@ -539,61 +708,207 @@ struct NOST : public ContractBase qpi.transferShareOwnershipAndPossession(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, SELF, SELF, locals.lotItem.quantity, qpi.invocator()); } + output.success = 0; + return; + } + } + } + + PRIVATE_FUNCTION_WITH_LOCALS(BuildAuctionData) + { + output.auction.allowedBidderWallets.reset(); + output.auction.auctionId = id::randomValue(); + output.auction.quantityForSale = input.quantityForSale; + output.auction.allocatedQuantity = 0; + output.auction.minimumPurchaseQuantity = input.minimumPurchaseQuantity; + output.auction.initialPricePerUnit = input.createInput.initialPricePerUnit; + output.auction.salePricePerUnit = input.createInput.salePricePerUnit; + output.auction.minimumBidIncrement = input.createInput.minimumBidIncrementPerUnit; + output.auction.buyNowPricePerUnit = input.createInput.buyNowPricePerUnit; + output.auction.highestBidPerUnit = 0; + output.auction.highestBidQuantity = 0; + output.auction.highestBidAmount = 0; + output.auction.auctionDurationSeconds = smul(static_cast(input.createInput.durationDays), NOST_SECONDS_PER_DAY); + output.auction.createdAt = qpi.now(); + output.auction.lastBidAt = output.auction.createdAt; + output.auction.sellerDecisionDeadline = DateAndTime(); + output.auction.settledAt = DateAndTime(); + output.auction.bidderCount = 0; + output.auction.seller = qpi.invocator(); + output.auction.highestBidder = NULL_ID; + output.auction.assetForSale = input.firstLotItem.asset; + output.auction.requiredAccessAsset = input.createInput.requiredAccessAsset; + output.auction.auctionLotItems = input.createInput.auctionLotItems; + for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < NOST_AUCTION_ALLOWED_WALLET_NUM; ++locals.allowedWalletIndex) + { + if (!isZero(input.createInput.allowedBidderWallets.get(locals.allowedWalletIndex))) + { + output.auction.allowedBidderWallets.add(input.createInput.allowedBidderWallets.get(locals.allowedWalletIndex)); + } + } + output.auction.metadataIpfsCid = input.createInput.metadataIpfsCid; + output.auction.type = input.auctionType; + output.auction.visibility = input.visibility; + output.auction.status = EAuctionStatus::Active; + } + + PUBLIC_PROCEDURE_WITH_LOCALS(CreateAuction) + { + output.errorCode = static_cast(EAuctionError::InvalidInput); + + locals.auctionType = static_cast(input.auctionType); + locals.visibility = static_cast(input.auctionVisibility); + + if (!isSupportedAuctionType(locals.auctionType)) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::InvalidAuctionType); + return; + } + + if (!isSupportedAuctionVisibility(locals.visibility)) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::InvalidVisibility); + return; + } + + if (state.get().auctionList.population() >= state.get().auctionList.capacity()) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::StorageFull); + 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()); + } + return; + } + locals.resolvedQuantityForSale = 0; + locals.resolvedMinimumPurchaseQuantity = 0; + switch (locals.auctionType) + { + case EAuctionType::Batch: + locals.resolveBatchParamsInput.lotItemCount = locals.analyzeAuctionLotOutput.lotItemCount; + locals.resolveBatchParamsInput.totalEscrowQuantity = locals.analyzeAuctionLotOutput.totalEscrowQuantity; + locals.resolveBatchParamsInput.minimumPurchaseQuantity = input.minimumPurchaseQuantity; + CALL(ResolveBatchAuctionCreateParams, locals.resolveBatchParamsInput, locals.resolveBatchParamsOutput); + if (!locals.resolveBatchParamsOutput.isValid) + { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::InsufficientAssetBalance); return; } + locals.resolvedQuantityForSale = locals.resolveBatchParamsOutput.quantityForSale; + locals.resolvedMinimumPurchaseQuantity = locals.resolveBatchParamsOutput.minimumPurchaseQuantity; + break; + case EAuctionType::Standard: + locals.resolveStandardParamsInput.minimumPurchaseQuantity = input.minimumPurchaseQuantity; + locals.resolveStandardParamsInput.minimumBidIncrementPerUnit = input.minimumBidIncrementPerUnit; + CALL(ResolveStandardAuctionCreateParams, locals.resolveStandardParamsInput, locals.resolveStandardParamsOutput); + if (!locals.resolveStandardParamsOutput.isValid) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + locals.resolvedQuantityForSale = locals.resolveStandardParamsOutput.quantityForSale; + locals.resolvedMinimumPurchaseQuantity = locals.resolveStandardParamsOutput.minimumPurchaseQuantity; + break; + default: + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::InvalidAuctionType); + return; } - locals.auction.auctionId = id::randomValue(); - locals.auction.quantityForSale = (locals.auctionType == EAuctionType::Standard) ? 1ULL : locals.totalEscrowQuantity; - locals.auction.allocatedQuantity = 0; - locals.auction.minimumPurchaseQuantity = (locals.auctionType == EAuctionType::Standard) ? 1ULL : input.minimumPurchaseQuantity; - locals.auction.initialPricePerUnit = input.initialPricePerUnit; - locals.auction.salePricePerUnit = input.salePricePerUnit; - locals.auction.minimumBidIncrement = input.minimumBidIncrementPerUnit; - locals.auction.buyNowPricePerUnit = input.buyNowPricePerUnit; - locals.auction.highestBidPerUnit = 0; - locals.auction.highestBidQuantity = 0; - locals.auction.highestBidAmount = 0; - locals.auction.auctionDurationSeconds = smul(static_cast(input.durationDays), NOST_SECONDS_PER_DAY); - locals.auction.createdAt = qpi.now(); - locals.auction.lastBidAt = locals.auction.createdAt; - locals.auction.sellerDecisionDeadline = DateAndTime(); - locals.auction.settledAt = DateAndTime(); - locals.auction.bidderCount = 0; - locals.auction.seller = qpi.invocator(); - locals.auction.highestBidder = NULL_ID; - locals.auction.assetForSale = locals.firstLotItem.asset; - locals.auction.requiredAccessAsset = input.requiredAccessAsset; - locals.auction.auctionLotItems = input.auctionLotItems; - for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < NOST_AUCTION_ALLOWED_WALLET_NUM; ++locals.allowedWalletIndex) + locals.countAllowedBidderWalletsInput.allowedBidderWallets = input.allowedBidderWallets; + CALL(CountAllowedBidderWallets, locals.countAllowedBidderWalletsInput, locals.countAllowedBidderWalletsOutput); + locals.validatePrivateAuctionAccessInput.visibility = locals.visibility; + locals.validatePrivateAuctionAccessInput.requiredAccessAsset = input.requiredAccessAsset; + locals.validatePrivateAuctionAccessInput.allowedWalletCount = locals.countAllowedBidderWalletsOutput.allowedWalletCount; + CALL(ValidatePrivateAuctionAccess, locals.validatePrivateAuctionAccessInput, locals.validatePrivateAuctionAccessOutput); + if (!locals.validatePrivateAuctionAccessOutput.isValid) { - if (!isZero(input.allowedBidderWallets.get(locals.allowedWalletIndex))) + if (qpi.invocationReward() > 0) { - locals.auction.allowedBidderWallets.add(input.allowedBidderWallets.get(locals.allowedWalletIndex)); + qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + return; } - locals.auction.metadataIpfsCid = input.metadataIpfsCid; - locals.auction.type = locals.auctionType; - locals.auction.visibility = locals.visibility; - locals.auction.status = EAuctionStatus::Active; - if (state.mut().auctionList.set(locals.auction.auctionId, locals.auction) == NULL_INDEX) + locals.getCreateAuctionFeeInput.visibility = locals.visibility; + CALL(GetCreateAuctionFee, locals.getCreateAuctionFeeInput, locals.getCreateAuctionFeeOutput); + locals.requiredFee = locals.getCreateAuctionFeeOutput.requiredFee; + if (qpi.invocationReward() < locals.requiredFee) { - for (locals.rollbackLotItemIndex = 0; locals.rollbackLotItemIndex < NOST_AUCTION_LOT_ITEM_NUM; ++locals.rollbackLotItemIndex) + if (qpi.invocationReward() > 0) { - 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()); + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::InsufficientFunds); + 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()); + } + output.errorCode = static_cast(EAuctionError::InsufficientAssetBalance); + return; + } + + 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 = static_cast(EAuctionError::InsufficientAssetBalance); + return; + } + + locals.buildAuctionDataInput.createInput = input; + locals.buildAuctionDataInput.firstLotItem = locals.analyzeAuctionLotOutput.firstLotItem; + locals.buildAuctionDataInput.auctionType = locals.auctionType; + locals.buildAuctionDataInput.visibility = locals.visibility; + locals.buildAuctionDataInput.quantityForSale = locals.resolvedQuantityForSale; + locals.buildAuctionDataInput.minimumPurchaseQuantity = locals.resolvedMinimumPurchaseQuantity; + CALL(BuildAuctionData, locals.buildAuctionDataInput, locals.buildAuctionDataOutput); + locals.auction = locals.buildAuctionDataOutput.auction; + + if (state.mut().auctionList.set(locals.auction.auctionId, locals.auction) == NULL_INDEX) + { + locals.rollbackAuctionLotAssetsInput.auctionLotItems = input.auctionLotItems; + CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); From 760d2371828126c55dd468117bc46522a985ecc1 Mon Sep 17 00:00:00 2001 From: N-010 Date: Tue, 14 Apr 2026 23:31:54 +0300 Subject: [PATCH 03/59] Refactor `Nostromo` contract: remove obsolete structs and functions, simplify auction initialization, modularize parameter resolution, and enforce duration cap validation. --- src/contracts/Nostromo.h | 380 ++++++++++++--------------------------- 1 file changed, 119 insertions(+), 261 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index e3f11e463..83f3914ff 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -5,7 +5,8 @@ constexpr uint64 NOST_AUCTION_METADATA_CID_LENGTH = 64; constexpr uint64 NOST_AUCTION_PARTICIPANT_NUM = 4096; constexpr uint64 NOST_AUCTION_LOT_ITEM_NUM = 64; constexpr uint64 NOST_AUCTION_ALLOWED_WALLET_NUM = 128; -constexpr uint64 NOST_PRIVATE_AUCTION_FEE = 50000000ULL; +constexpr uint32 NOST_AUCTION_MAX_DURATION_DAYS = 30; +constexpr sint64 NOST_PRIVATE_AUCTION_FEE = 50000000LL; constexpr uint64 NOST_AUCTION_EXTENSION_SECONDS = 300ULL; constexpr uint64 NOST_SECONDS_PER_DAY = 86400ULL; constexpr uint64 NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS = 604800ULL; @@ -169,9 +170,6 @@ struct NOST : public ContractBase /// Wallet that currently holds the highest bid. id highestBidder; - /// Primary asset reference of the lot, equal to the first non-empty lot entry. - Asset assetForSale; - /// Asset required for participation when the auction visibility is private. Asset requiredAccessAsset; @@ -196,6 +194,12 @@ struct NOST : public ContractBase struct StateData { + /// Configured fee charged when creating a private auction. + sint64 privateAuctionFee; + + /// Configured maximum auction duration in days. + uint32 maxAuctionDurationDays; + HashMap auctionList; HashMap participants; }; @@ -229,7 +233,7 @@ struct NOST : public ContractBase /// Buy Now price that immediately closes a standard auction once matched or exceeded. uint64 buyNowPricePerUnit; - /// Auction duration configured by the seller in days. + /// Auction duration configured by the seller in days, capped by the contract configuration. uint32 durationDays; /// Auction House mode selected by the seller: Batch Auction or Standard Auction. @@ -281,41 +285,6 @@ struct NOST : public ContractBase uint8 found; }; - struct ResolveBatchAuctionCreateParams_input - { - uint64 lotItemCount; - uint64 totalEscrowQuantity; - uint64 minimumPurchaseQuantity; - }; - - struct ResolveBatchAuctionCreateParams_output - { - uint64 quantityForSale; - uint64 minimumPurchaseQuantity; - uint8 isValid; - }; - - struct ResolveBatchAuctionCreateParams_locals - { - }; - - struct ResolveStandardAuctionCreateParams_input - { - uint64 minimumPurchaseQuantity; - uint64 minimumBidIncrementPerUnit; - }; - - struct ResolveStandardAuctionCreateParams_output - { - uint64 quantityForSale; - uint64 minimumPurchaseQuantity; - uint8 isValid; - }; - - struct ResolveStandardAuctionCreateParams_locals - { - }; - struct AnalyzeAuctionLot_input { Array auctionLotItems; @@ -326,7 +295,6 @@ struct NOST : public ContractBase { uint64 totalEscrowQuantity; uint64 lotItemCount; - AuctionLotEntry firstLotItem; uint8 isValid; }; @@ -351,36 +319,6 @@ struct NOST : public ContractBase uint64 allowedWalletIndex; }; - struct ValidatePrivateAuctionAccess_input - { - EAuctionVisibility visibility; - Asset requiredAccessAsset; - uint64 allowedWalletCount; - }; - - struct ValidatePrivateAuctionAccess_output - { - uint8 isValid; - }; - - struct ValidatePrivateAuctionAccess_locals - { - }; - - struct GetCreateAuctionFee_input - { - EAuctionVisibility visibility; - }; - - struct GetCreateAuctionFee_output - { - sint64 requiredFee; - }; - - struct GetCreateAuctionFee_locals - { - }; - struct VerifyAuctionLotBalances_input { Array auctionLotItems; @@ -421,9 +359,7 @@ struct NOST : public ContractBase Array auctionLotItems; }; - struct RollbackAuctionLotAssets_output - { - }; + typedef NoData RollbackAuctionLotAssets_output; struct RollbackAuctionLotAssets_locals { @@ -431,26 +367,6 @@ struct NOST : public ContractBase uint64 lotItemIndex; }; - struct BuildAuctionData_input - { - CreateAuction_input createInput; - AuctionLotEntry firstLotItem; - EAuctionType auctionType; - EAuctionVisibility visibility; - uint64 quantityForSale; - uint64 minimumPurchaseQuantity; - }; - - struct BuildAuctionData_output - { - AuctionData auction; - }; - - struct BuildAuctionData_locals - { - uint64 allowedWalletIndex; - }; - struct CreateAuction_locals { AuctionData auction; @@ -458,27 +374,16 @@ struct NOST : public ContractBase AnalyzeAuctionLot_output analyzeAuctionLotOutput; CountAllowedBidderWallets_input countAllowedBidderWalletsInput; CountAllowedBidderWallets_output countAllowedBidderWalletsOutput; - ValidatePrivateAuctionAccess_input validatePrivateAuctionAccessInput; - ValidatePrivateAuctionAccess_output validatePrivateAuctionAccessOutput; - GetCreateAuctionFee_input getCreateAuctionFeeInput; - GetCreateAuctionFee_output getCreateAuctionFeeOutput; VerifyAuctionLotBalances_input verifyAuctionLotBalancesInput; - VerifyAuctionLotBalances_output verifyAuctionLotBalancesOutput; EscrowAuctionLotAssets_input escrowAuctionLotAssetsInput; - EscrowAuctionLotAssets_output escrowAuctionLotAssetsOutput; RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; - RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; - BuildAuctionData_input buildAuctionDataInput; - BuildAuctionData_output buildAuctionDataOutput; - ResolveBatchAuctionCreateParams_input resolveBatchParamsInput; - ResolveBatchAuctionCreateParams_output resolveBatchParamsOutput; - ResolveStandardAuctionCreateParams_input resolveStandardParamsInput; - ResolveStandardAuctionCreateParams_output resolveStandardParamsOutput; - EAuctionType auctionType; - EAuctionVisibility visibility; sint64 requiredFee; uint64 resolvedQuantityForSale; uint64 resolvedMinimumPurchaseQuantity; + uint64 allowedWalletIndex; + RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; + EscrowAuctionLotAssets_output escrowAuctionLotAssetsOutput; + VerifyAuctionLotBalances_output verifyAuctionLotBalancesOutput; }; struct PlaceBid_locals @@ -512,13 +417,17 @@ struct NOST : public ContractBase { REGISTER_USER_PROCEDURE(CreateAuction, 1); REGISTER_USER_PROCEDURE(PlaceBid, 2); + REGISTER_USER_PROCEDURE(TransferShareManagementRights, 3); + REGISTER_USER_FUNCTION(GetAuction, 1); REGISTER_USER_FUNCTION(GetAuctionParticipant, 2); - - REGISTER_USER_PROCEDURE(TransferShareManagementRights, 1); } - INITIALIZE() {} + INITIALIZE() + { + state.mut().privateAuctionFee = NOST_PRIVATE_AUCTION_FEE; + state.mut().maxAuctionDurationDays = NOST_AUCTION_MAX_DURATION_DAYS; + } PRE_ACQUIRE_SHARES() { @@ -532,6 +441,8 @@ struct NOST : public ContractBase if (qpi.epoch() == 220) { // Initialize + state.mut().privateAuctionFee = NOST_PRIVATE_AUCTION_FEE; + state.mut().maxAuctionDurationDays = NOST_AUCTION_MAX_DURATION_DAYS; } } @@ -541,45 +452,13 @@ struct NOST : public ContractBase state.mut().participants.cleanupIfNeeded(); } - PRIVATE_FUNCTION_WITH_LOCALS(ResolveBatchAuctionCreateParams) - { - output.quantityForSale = 0; - output.minimumPurchaseQuantity = 0; - output.isValid = 0; - - if (input.lotItemCount != 1 || input.minimumPurchaseQuantity == 0 || input.minimumPurchaseQuantity > input.totalEscrowQuantity) - { - return; - } - - output.quantityForSale = input.totalEscrowQuantity; - output.minimumPurchaseQuantity = input.minimumPurchaseQuantity; - output.isValid = 1; - } - - PRIVATE_FUNCTION_WITH_LOCALS(ResolveStandardAuctionCreateParams) - { - output.quantityForSale = 0; - output.minimumPurchaseQuantity = 0; - output.isValid = 0; - - if (input.minimumPurchaseQuantity > 1 || input.minimumBidIncrementPerUnit == 0) - { - return; - } - - output.quantityForSale = 1; - output.minimumPurchaseQuantity = 1; - output.isValid = 1; - } - PRIVATE_FUNCTION_WITH_LOCALS(AnalyzeAuctionLot) { output.totalEscrowQuantity = 0; output.lotItemCount = 0; output.isValid = 0; - if (input.durationDays == 0) + if (input.durationDays == 0 || input.durationDays > state.get().maxAuctionDurationDays) { return; } @@ -601,11 +480,6 @@ struct NOST : public ContractBase return; } - if (output.lotItemCount == 0) - { - output.firstLotItem = locals.lotItem; - } - output.lotItemCount = sadd(output.lotItemCount, 1ULL); output.totalEscrowQuantity = sadd(output.totalEscrowQuantity, static_cast(locals.lotItem.quantity)); } @@ -625,24 +499,6 @@ struct NOST : public ContractBase } } - PRIVATE_FUNCTION_WITH_LOCALS(ValidatePrivateAuctionAccess) - { - output.isValid = 1; - if (input.visibility == EAuctionVisibility::Private && isZeroAsset(input.requiredAccessAsset) && input.allowedWalletCount == 0) - { - output.isValid = 0; - } - } - - PRIVATE_FUNCTION_WITH_LOCALS(GetCreateAuctionFee) - { - output.requiredFee = 0; - if (input.visibility == EAuctionVisibility::Private) - { - output.requiredFee = NOST_PRIVATE_AUCTION_FEE; - } - } - PRIVATE_FUNCTION_WITH_LOCALS(VerifyAuctionLotBalances) { output.hasEnoughBalance = 1; @@ -655,7 +511,7 @@ struct NOST : public ContractBase } locals.possessedShares = qpi.numberOfPossessedShares(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, qpi.invocator(), - qpi.invocator(), SELF_INDEX, SELF_INDEX); + qpi.invocator(), SELF_INDEX, SELF_INDEX); if (locals.possessedShares < locals.lotItem.quantity) { output.hasEnoughBalance = 0; @@ -678,7 +534,7 @@ struct NOST : public ContractBase } } - PRIVATE_FUNCTION_WITH_LOCALS(EscrowAuctionLotAssets) + PRIVATE_PROCEDURE_WITH_LOCALS(EscrowAuctionLotAssets) { output.success = 1; for (locals.lotItemIndex = 0; locals.lotItemIndex < NOST_AUCTION_LOT_ITEM_NUM; ++locals.lotItemIndex) @@ -714,52 +570,11 @@ struct NOST : public ContractBase } } - PRIVATE_FUNCTION_WITH_LOCALS(BuildAuctionData) - { - output.auction.allowedBidderWallets.reset(); - output.auction.auctionId = id::randomValue(); - output.auction.quantityForSale = input.quantityForSale; - output.auction.allocatedQuantity = 0; - output.auction.minimumPurchaseQuantity = input.minimumPurchaseQuantity; - output.auction.initialPricePerUnit = input.createInput.initialPricePerUnit; - output.auction.salePricePerUnit = input.createInput.salePricePerUnit; - output.auction.minimumBidIncrement = input.createInput.minimumBidIncrementPerUnit; - output.auction.buyNowPricePerUnit = input.createInput.buyNowPricePerUnit; - output.auction.highestBidPerUnit = 0; - output.auction.highestBidQuantity = 0; - output.auction.highestBidAmount = 0; - output.auction.auctionDurationSeconds = smul(static_cast(input.createInput.durationDays), NOST_SECONDS_PER_DAY); - output.auction.createdAt = qpi.now(); - output.auction.lastBidAt = output.auction.createdAt; - output.auction.sellerDecisionDeadline = DateAndTime(); - output.auction.settledAt = DateAndTime(); - output.auction.bidderCount = 0; - output.auction.seller = qpi.invocator(); - output.auction.highestBidder = NULL_ID; - output.auction.assetForSale = input.firstLotItem.asset; - output.auction.requiredAccessAsset = input.createInput.requiredAccessAsset; - output.auction.auctionLotItems = input.createInput.auctionLotItems; - for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < NOST_AUCTION_ALLOWED_WALLET_NUM; ++locals.allowedWalletIndex) - { - if (!isZero(input.createInput.allowedBidderWallets.get(locals.allowedWalletIndex))) - { - output.auction.allowedBidderWallets.add(input.createInput.allowedBidderWallets.get(locals.allowedWalletIndex)); - } - } - output.auction.metadataIpfsCid = input.createInput.metadataIpfsCid; - output.auction.type = input.auctionType; - output.auction.visibility = input.visibility; - output.auction.status = EAuctionStatus::Active; - } - PUBLIC_PROCEDURE_WITH_LOCALS(CreateAuction) { output.errorCode = static_cast(EAuctionError::InvalidInput); - locals.auctionType = static_cast(input.auctionType); - locals.visibility = static_cast(input.auctionVisibility); - - if (!isSupportedAuctionType(locals.auctionType)) + if (!isSupportedAuctionType(static_cast(input.auctionType))) { if (qpi.invocationReward() > 0) { @@ -769,7 +584,7 @@ struct NOST : public ContractBase return; } - if (!isSupportedAuctionVisibility(locals.visibility)) + if (!isSupportedAuctionVisibility(static_cast(input.auctionVisibility))) { if (qpi.invocationReward() > 0) { @@ -802,55 +617,44 @@ struct NOST : public ContractBase } locals.resolvedQuantityForSale = 0; locals.resolvedMinimumPurchaseQuantity = 0; - switch (locals.auctionType) + switch (static_cast(input.auctionType)) { - case EAuctionType::Batch: - locals.resolveBatchParamsInput.lotItemCount = locals.analyzeAuctionLotOutput.lotItemCount; - locals.resolveBatchParamsInput.totalEscrowQuantity = locals.analyzeAuctionLotOutput.totalEscrowQuantity; - locals.resolveBatchParamsInput.minimumPurchaseQuantity = input.minimumPurchaseQuantity; - CALL(ResolveBatchAuctionCreateParams, locals.resolveBatchParamsInput, locals.resolveBatchParamsOutput); - if (!locals.resolveBatchParamsOutput.isValid) - { - if (qpi.invocationReward() > 0) + case EAuctionType::Batch: + if (!resolveBatchAuctionCreateParams(locals.analyzeAuctionLotOutput.lotItemCount, locals.analyzeAuctionLotOutput.totalEscrowQuantity, + input.minimumPurchaseQuantity, locals.resolvedQuantityForSale, + locals.resolvedMinimumPurchaseQuantity)) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; } - return; - } - locals.resolvedQuantityForSale = locals.resolveBatchParamsOutput.quantityForSale; - locals.resolvedMinimumPurchaseQuantity = locals.resolveBatchParamsOutput.minimumPurchaseQuantity; - break; - case EAuctionType::Standard: - locals.resolveStandardParamsInput.minimumPurchaseQuantity = input.minimumPurchaseQuantity; - locals.resolveStandardParamsInput.minimumBidIncrementPerUnit = input.minimumBidIncrementPerUnit; - CALL(ResolveStandardAuctionCreateParams, locals.resolveStandardParamsInput, locals.resolveStandardParamsOutput); - if (!locals.resolveStandardParamsOutput.isValid) - { + break; + case EAuctionType::Standard: + if (!resolveStandardAuctionCreateParams(input.minimumPurchaseQuantity, input.minimumBidIncrementPerUnit, + locals.resolvedQuantityForSale, locals.resolvedMinimumPurchaseQuantity)) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + break; + default: if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + output.errorCode = static_cast(EAuctionError::InvalidAuctionType); return; - } - locals.resolvedQuantityForSale = locals.resolveStandardParamsOutput.quantityForSale; - locals.resolvedMinimumPurchaseQuantity = locals.resolveStandardParamsOutput.minimumPurchaseQuantity; - break; - default: - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = static_cast(EAuctionError::InvalidAuctionType); - return; } locals.countAllowedBidderWalletsInput.allowedBidderWallets = input.allowedBidderWallets; CALL(CountAllowedBidderWallets, locals.countAllowedBidderWalletsInput, locals.countAllowedBidderWalletsOutput); - locals.validatePrivateAuctionAccessInput.visibility = locals.visibility; - locals.validatePrivateAuctionAccessInput.requiredAccessAsset = input.requiredAccessAsset; - locals.validatePrivateAuctionAccessInput.allowedWalletCount = locals.countAllowedBidderWalletsOutput.allowedWalletCount; - CALL(ValidatePrivateAuctionAccess, locals.validatePrivateAuctionAccessInput, locals.validatePrivateAuctionAccessOutput); - if (!locals.validatePrivateAuctionAccessOutput.isValid) + if (!validatePrivateAuctionAccess(static_cast(input.auctionVisibility), input.requiredAccessAsset, + locals.countAllowedBidderWalletsOutput.allowedWalletCount)) { if (qpi.invocationReward() > 0) { @@ -859,9 +663,7 @@ struct NOST : public ContractBase return; } - locals.getCreateAuctionFeeInput.visibility = locals.visibility; - CALL(GetCreateAuctionFee, locals.getCreateAuctionFeeInput, locals.getCreateAuctionFeeOutput); - locals.requiredFee = locals.getCreateAuctionFeeOutput.requiredFee; + locals.requiredFee = getCreateAuctionFee(static_cast(input.auctionVisibility), state); if (qpi.invocationReward() < locals.requiredFee) { if (qpi.invocationReward() > 0) @@ -896,14 +698,32 @@ struct NOST : public ContractBase return; } - locals.buildAuctionDataInput.createInput = input; - locals.buildAuctionDataInput.firstLotItem = locals.analyzeAuctionLotOutput.firstLotItem; - locals.buildAuctionDataInput.auctionType = locals.auctionType; - locals.buildAuctionDataInput.visibility = locals.visibility; - locals.buildAuctionDataInput.quantityForSale = locals.resolvedQuantityForSale; - locals.buildAuctionDataInput.minimumPurchaseQuantity = locals.resolvedMinimumPurchaseQuantity; - CALL(BuildAuctionData, locals.buildAuctionDataInput, locals.buildAuctionDataOutput); - locals.auction = locals.buildAuctionDataOutput.auction; + locals.auction.auctionId = id::randomValue(); + locals.auction.quantityForSale = locals.resolvedQuantityForSale; + locals.auction.minimumPurchaseQuantity = locals.resolvedMinimumPurchaseQuantity; + locals.auction.initialPricePerUnit = input.initialPricePerUnit; + locals.auction.salePricePerUnit = input.salePricePerUnit; + locals.auction.minimumBidIncrement = input.minimumBidIncrementPerUnit; + locals.auction.buyNowPricePerUnit = input.buyNowPricePerUnit; + locals.auction.auctionDurationSeconds = smul(static_cast(input.durationDays), NOST_SECONDS_PER_DAY); + locals.auction.createdAt = qpi.now(); + locals.auction.lastBidAt = locals.auction.createdAt; + locals.auction.sellerDecisionDeadline = DateAndTime(); + locals.auction.settledAt = DateAndTime(); + locals.auction.seller = qpi.invocator(); + locals.auction.requiredAccessAsset = input.requiredAccessAsset; + locals.auction.auctionLotItems = input.auctionLotItems; + for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < NOST_AUCTION_ALLOWED_WALLET_NUM; ++locals.allowedWalletIndex) + { + if (!isZero(input.allowedBidderWallets.get(locals.allowedWalletIndex))) + { + locals.auction.allowedBidderWallets.add(input.allowedBidderWallets.get(locals.allowedWalletIndex)); + } + } + locals.auction.metadataIpfsCid = input.metadataIpfsCid; + locals.auction.type = static_cast(input.auctionType); + locals.auction.visibility = static_cast(input.auctionVisibility); + locals.auction.status = EAuctionStatus::Active; if (state.mut().auctionList.set(locals.auction.auctionId, locals.auction) == NULL_INDEX) { @@ -1185,6 +1005,44 @@ struct NOST : public ContractBase } protected: + static bool resolveBatchAuctionCreateParams(uint64 lotItemCount, uint64 totalEscrowQuantity, uint64 minimumPurchaseQuantity, + uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity) + { + quantityForSale = 0; + resolvedMinimumPurchaseQuantity = 0; + if (lotItemCount != 1 || minimumPurchaseQuantity == 0 || minimumPurchaseQuantity > totalEscrowQuantity) + { + return false; + } + quantityForSale = totalEscrowQuantity; + resolvedMinimumPurchaseQuantity = minimumPurchaseQuantity; + return true; + } + + static bool resolveStandardAuctionCreateParams(uint64 minimumPurchaseQuantity, uint64 minimumBidIncrementPerUnit, uint64& quantityForSale, + uint64& resolvedMinimumPurchaseQuantity) + { + quantityForSale = 0; + resolvedMinimumPurchaseQuantity = 0; + if (minimumPurchaseQuantity > 1 || minimumBidIncrementPerUnit == 0) + { + return false; + } + quantityForSale = 1; + resolvedMinimumPurchaseQuantity = 1; + return true; + } + + static bool validatePrivateAuctionAccess(EAuctionVisibility visibility, const Asset& requiredAccessAsset, uint64 allowedWalletCount) + { + return visibility != EAuctionVisibility::Private || !isZeroAsset(requiredAccessAsset) || allowedWalletCount > 0; + } + + static sint64 getCreateAuctionFee(EAuctionVisibility visibility, const ContractState& state) + { + return visibility == EAuctionVisibility::Private ? state.get().privateAuctionFee : 0; + } + static bool isSupportedAuctionType(EAuctionType auctionType) { return auctionType == EAuctionType::Batch || auctionType == EAuctionType::Standard; From 27af3e971efc3e54c9e1781520086ab6e77c5039 Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 15 Apr 2026 00:02:47 +0300 Subject: [PATCH 04/59] Add `ValidateMetadataCid` procedure to `Nostromo` contract: introduce IPFS CID validation with character checks and integration into `CreateAuction` function. --- src/contracts/Nostromo.h | 73 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 83f3914ff..3bdb37652 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -319,6 +319,24 @@ struct NOST : public ContractBase uint64 allowedWalletIndex; }; + struct ValidateMetadataCid_input + { + Array metadataIpfsCid; + }; + + struct ValidateMetadataCid_output + { + uint8 isValid; + }; + + struct ValidateMetadataCid_locals + { + uint64 cidIndex; + uint8 cidChar; + uint8 hasPayloadCharacters; + uint8 reachedTerminator; + }; + struct VerifyAuctionLotBalances_input { Array auctionLotItems; @@ -370,6 +388,8 @@ struct NOST : public ContractBase struct CreateAuction_locals { AuctionData auction; + ValidateMetadataCid_input validateMetadataCidInput; + ValidateMetadataCid_output validateMetadataCidOutput; AnalyzeAuctionLot_input analyzeAuctionLotInput; AnalyzeAuctionLot_output analyzeAuctionLotOutput; CountAllowedBidderWallets_input countAllowedBidderWalletsInput; @@ -499,6 +519,48 @@ struct NOST : public ContractBase } } + PRIVATE_FUNCTION_WITH_LOCALS(ValidateMetadataCid) + { + output.isValid = 0; + locals.hasPayloadCharacters = 0; + locals.reachedTerminator = 0; + + if (input.metadataIpfsCid.get(0) != 'b') + { + return; + } + + for (locals.cidIndex = 1; locals.cidIndex < NOST_AUCTION_METADATA_CID_LENGTH; ++locals.cidIndex) + { + locals.cidChar = input.metadataIpfsCid.get(locals.cidIndex); + if (locals.cidChar == 0) + { + locals.reachedTerminator = 1; + continue; + } + + if (locals.reachedTerminator) + { + return; + } + + if ((locals.cidChar >= 'a' && locals.cidChar <= 'z') || (locals.cidChar >= '2' && locals.cidChar <= '7')) + { + locals.hasPayloadCharacters = 1; + continue; + } + + return; + } + + if (!locals.hasPayloadCharacters) + { + return; + } + + output.isValid = 1; + } + PRIVATE_FUNCTION_WITH_LOCALS(VerifyAuctionLotBalances) { output.hasEnoughBalance = 1; @@ -604,6 +666,17 @@ struct NOST : public ContractBase 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()); + } + return; + } + locals.analyzeAuctionLotInput.auctionLotItems = input.auctionLotItems; locals.analyzeAuctionLotInput.durationDays = input.durationDays; CALL(AnalyzeAuctionLot, locals.analyzeAuctionLotInput, locals.analyzeAuctionLotOutput); From 6f94648dbdcbe114c915d5d97349389f3d38e10c Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 15 Apr 2026 00:11:36 +0300 Subject: [PATCH 05/59] Add buy-now and price validation to auction parameter resolution in `Nostromo`: enhance `resolveBatchAuctionCreateParams` and `resolveStandardAuctionCreateParams` with price-related inputs and validation logic. --- src/contracts/Nostromo.h | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 3bdb37652..a1a4df885 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -693,9 +693,10 @@ struct NOST : public ContractBase switch (static_cast(input.auctionType)) { case EAuctionType::Batch: + if (!resolveBatchAuctionCreateParams(locals.analyzeAuctionLotOutput.lotItemCount, locals.analyzeAuctionLotOutput.totalEscrowQuantity, input.minimumPurchaseQuantity, locals.resolvedQuantityForSale, - locals.resolvedMinimumPurchaseQuantity)) + locals.resolvedMinimumPurchaseQuantity, input.buyNowPricePerUnit)) { if (qpi.invocationReward() > 0) { @@ -706,7 +707,8 @@ struct NOST : public ContractBase break; case EAuctionType::Standard: if (!resolveStandardAuctionCreateParams(input.minimumPurchaseQuantity, input.minimumBidIncrementPerUnit, - locals.resolvedQuantityForSale, locals.resolvedMinimumPurchaseQuantity)) + locals.resolvedQuantityForSale, locals.resolvedMinimumPurchaseQuantity, + input.buyNowPricePerUnit, input.initialPricePerUnit, input.salePricePerUnit)) { if (qpi.invocationReward() > 0) { @@ -1079,11 +1081,11 @@ struct NOST : public ContractBase protected: static bool resolveBatchAuctionCreateParams(uint64 lotItemCount, uint64 totalEscrowQuantity, uint64 minimumPurchaseQuantity, - uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity) + uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPricePerUnit) { quantityForSale = 0; resolvedMinimumPurchaseQuantity = 0; - if (lotItemCount != 1 || minimumPurchaseQuantity == 0 || minimumPurchaseQuantity > totalEscrowQuantity) + if (lotItemCount != 1 || minimumPurchaseQuantity == 0 || minimumPurchaseQuantity > totalEscrowQuantity || buyNowPricePerUnit != 0) { return false; } @@ -1093,7 +1095,8 @@ struct NOST : public ContractBase } static bool resolveStandardAuctionCreateParams(uint64 minimumPurchaseQuantity, uint64 minimumBidIncrementPerUnit, uint64& quantityForSale, - uint64& resolvedMinimumPurchaseQuantity) + uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPricePerUnit, uint64 initialPricePerUnit, + uint64 salePricePerUnit) { quantityForSale = 0; resolvedMinimumPurchaseQuantity = 0; @@ -1101,6 +1104,13 @@ struct NOST : public ContractBase { return false; } + + if (buyNowPricePerUnit > 0 && (buyNowPricePerUnit < initialPricePerUnit || buyNowPricePerUnit < salePricePerUnit)) + { + + return false; + } + quantityForSale = 1; resolvedMinimumPurchaseQuantity = 1; return true; From 4fa25385cd157457c8b882c3653442eff1b26cff Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 15 Apr 2026 00:14:05 +0300 Subject: [PATCH 06/59] Add sale price validation in `ResolveBatchAuctionCloseParams` of `Nostromo`: ensure `salePricePerUnit` is non-zero and less than or equal to `initialPricePerUnit`. --- src/contracts/Nostromo.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index a1a4df885..931613ba1 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -1100,7 +1100,12 @@ struct NOST : public ContractBase { quantityForSale = 0; resolvedMinimumPurchaseQuantity = 0; - if (minimumPurchaseQuantity > 1 || minimumBidIncrementPerUnit == 0) + if (minimumPurchaseQuantity > 1 || minimumBidIncrementPerUnit == 0 || salePricePerUnit == 0) + { + return false; + } + + if (initialPricePerUnit > salePricePerUnit) { return false; } From 89c558aa4a98f956aebff66cdfc179162de48dcf Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 15 Apr 2026 00:23:00 +0300 Subject: [PATCH 07/59] Refactor `Nostromo` contract: remove unused auction fields, improve bidder validation, and refine access asset checks for private auctions. --- src/contracts/Nostromo.h | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 931613ba1..4db939fea 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -783,8 +783,6 @@ struct NOST : public ContractBase locals.auction.auctionDurationSeconds = smul(static_cast(input.durationDays), NOST_SECONDS_PER_DAY); locals.auction.createdAt = qpi.now(); locals.auction.lastBidAt = locals.auction.createdAt; - locals.auction.sellerDecisionDeadline = DateAndTime(); - locals.auction.settledAt = DateAndTime(); locals.auction.seller = qpi.invocator(); locals.auction.requiredAccessAsset = input.requiredAccessAsset; locals.auction.auctionLotItems = input.auctionLotItems; @@ -865,9 +863,10 @@ struct NOST : public ContractBase return; } - if (locals.auction.visibility == EAuctionVisibility::Private && - qpi.numberOfShares(locals.auction.requiredAccessAsset, AssetOwnershipSelect::byOwner(qpi.invocator()), - AssetPossessionSelect::byPossessor(qpi.invocator())) <= 0) + if (locals.auction.visibility == EAuctionVisibility::Private && !locals.auction.allowedBidderWallets.contains(qpi.invocator()) && + (isZeroAsset(locals.auction.requiredAccessAsset) || + qpi.numberOfShares(locals.auction.requiredAccessAsset, AssetOwnershipSelect::byOwner(qpi.invocator()), + AssetPossessionSelect::byPossessor(qpi.invocator())) <= 0)) { if (qpi.invocationReward() > 0) { From fdbfed2c978eada9da427448d7283e4eb7e1b2d7 Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 15 Apr 2026 00:53:25 +0300 Subject: [PATCH 08/59] Add auction cancellation functionality to `Nostromo`: introduce `CancelAuction` procedure with fee logic, participant refunds, and state updates; refactor auction fields to use total prices instead of per-unit pricing. --- src/contracts/Nostromo.h | 228 +++++++++++++++++++++++++++++++-------- 1 file changed, 185 insertions(+), 43 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 4db939fea..7eda84085 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -7,6 +7,7 @@ constexpr uint64 NOST_AUCTION_LOT_ITEM_NUM = 64; constexpr uint64 NOST_AUCTION_ALLOWED_WALLET_NUM = 128; constexpr uint32 NOST_AUCTION_MAX_DURATION_DAYS = 30; constexpr sint64 NOST_PRIVATE_AUCTION_FEE = 50000000LL; +constexpr uint64 NOST_AUCTION_CANCELLATION_FEE_BP = 1000ULL; constexpr uint64 NOST_AUCTION_EXTENSION_SECONDS = 300ULL; constexpr uint64 NOST_SECONDS_PER_DAY = 86400ULL; constexpr uint64 NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS = 604800ULL; @@ -81,13 +82,28 @@ struct NOST : public ContractBase */ struct AuctionParticipantData { + /// Amount currently locked in escrow for this participant bid. uint64 escrowedAmount; + + /// Quantity requested by this participant; standard auctions always use the whole lot quantity. uint64 requestedQuantity; + + /// Quantity finally allocated to this participant after batch settlement. uint64 allocatedQuantity; - uint64 pricePerUnit; + + /// Total bid amount committed by this participant for the current request. + uint64 bidAmount; + + /// Timestamp of the participant's latest accepted bid. DateAndTime lastBidTime; + + /// Wallet that owns this participant record. id participant; + + /// Marks the participant that currently holds the leading bid. uint8 isHighestBidder; + + /// Marks bids that remain inside the winning allocation after settlement. uint8 isWinningBid; }; @@ -125,25 +141,26 @@ struct NOST : public ContractBase /// Minimum quantity a bidder may request in a batch auction; standard auctions sell the entire lot as one unit. uint64 minimumPurchaseQuantity; - /// Initial Price for a standard auction; bids cannot start below this value. - uint64 initialPricePerUnit; + /// Initial Price for a standard auction; bids cannot start below this total auction price. + uint64 initialPrice; - /// Sale Price defined by the seller as the desired / minimum acceptable selling price. - uint64 salePricePerUnit; + /// Sale Price defined by the seller as the desired / minimum acceptable total selling price. + uint64 salePrice; /// Minimum step by which a new bid must exceed the current highest bid. uint64 minimumBidIncrement; /// Buy Now price that closes a standard auction immediately when matched or exceeded. - uint64 buyNowPricePerUnit; + uint64 buyNowPrice; - /// Highest price per unit currently offered by any active bid. - uint64 highestBidPerUnit; + /// Highest total bid currently offered by any active bid. + uint64 highestBidPrice; /// Quantity requested by the current highest bid. uint64 highestBidQuantity; /// Total amount escrowed by the current highest bid. + /// Equal to the committed highest bid amount. uint64 highestBidAmount; /// Auction duration in seconds, derived from the duration configured in days. @@ -197,6 +214,9 @@ struct NOST : public ContractBase /// Configured fee charged when creating a private auction. sint64 privateAuctionFee; + /// Configured cancellation fee rate in basis points. + uint64 auctionCancellationFeeBasisPoints; + /// Configured maximum auction duration in days. uint32 maxAuctionDurationDays; @@ -221,17 +241,17 @@ struct NOST : public ContractBase /// Minimum quantity a bidder may request in a batch auction; standard auctions sell the whole lot as one unit. uint64 minimumPurchaseQuantity; - /// Initial Price for a standard auction; bids cannot be placed below this value. - uint64 initialPricePerUnit; + /// Initial Price for a standard auction; bids cannot be placed below this total auction price. + uint64 initialPrice; - /// Sale Price defined by the seller as the desired / minimum acceptable selling price. - uint64 salePricePerUnit; + /// Sale Price defined by the seller as the desired / minimum acceptable total selling price. + uint64 salePrice; /// Minimum step by which each new bid must exceed the current highest bid. - uint64 minimumBidIncrementPerUnit; + uint64 minimumBidIncrement; /// Buy Now price that immediately closes a standard auction once matched or exceeded. - uint64 buyNowPricePerUnit; + uint64 buyNowPrice; /// Auction duration configured by the seller in days, capped by the contract configuration. uint32 durationDays; @@ -251,9 +271,14 @@ struct NOST : public ContractBase struct PlaceBid_input { + /// Identifier of the target auction. id auctionId; + + /// Requested quantity for a batch auction; ignored for a standard auction because the whole lot is sold as one unit. uint64 quantity; - uint64 pricePerUnit; + + /// Total amount the bidder commits for this bid. + uint64 bidAmount; }; struct PlaceBid_output @@ -263,6 +288,18 @@ struct NOST : public ContractBase uint8 errorCode; }; + struct CancelAuction_input + { + id auctionId; + }; + + struct CancelAuction_output + { + uint64 refundedAmount; + uint64 cancellationFee; + uint8 errorCode; + }; + struct GetAuction_input { id auctionId; @@ -422,6 +459,18 @@ struct NOST : public ContractBase bool highestBidderExists; }; + struct CancelAuction_locals + { + AuctionData auction; + AuctionParticipantData participantData; + AuctionParticipantKey participantKey; + RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; + RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; + DateAndTime currentDate; + uint64 cancellationBaseAmount; + sint64 participantIndex; + }; + struct TransferShareManagementRights_input { Asset asset; @@ -437,7 +486,8 @@ struct NOST : public ContractBase { REGISTER_USER_PROCEDURE(CreateAuction, 1); REGISTER_USER_PROCEDURE(PlaceBid, 2); - REGISTER_USER_PROCEDURE(TransferShareManagementRights, 3); + REGISTER_USER_PROCEDURE(CancelAuction, 3); + REGISTER_USER_PROCEDURE(TransferShareManagementRights, 4); REGISTER_USER_FUNCTION(GetAuction, 1); REGISTER_USER_FUNCTION(GetAuctionParticipant, 2); @@ -446,6 +496,7 @@ struct NOST : public ContractBase INITIALIZE() { state.mut().privateAuctionFee = NOST_PRIVATE_AUCTION_FEE; + state.mut().auctionCancellationFeeBasisPoints = NOST_AUCTION_CANCELLATION_FEE_BP; state.mut().maxAuctionDurationDays = NOST_AUCTION_MAX_DURATION_DAYS; } @@ -462,6 +513,7 @@ struct NOST : public ContractBase { // Initialize state.mut().privateAuctionFee = NOST_PRIVATE_AUCTION_FEE; + state.mut().auctionCancellationFeeBasisPoints = NOST_AUCTION_CANCELLATION_FEE_BP; state.mut().maxAuctionDurationDays = NOST_AUCTION_MAX_DURATION_DAYS; } } @@ -696,7 +748,7 @@ struct NOST : public ContractBase if (!resolveBatchAuctionCreateParams(locals.analyzeAuctionLotOutput.lotItemCount, locals.analyzeAuctionLotOutput.totalEscrowQuantity, input.minimumPurchaseQuantity, locals.resolvedQuantityForSale, - locals.resolvedMinimumPurchaseQuantity, input.buyNowPricePerUnit)) + locals.resolvedMinimumPurchaseQuantity, input.buyNowPrice)) { if (qpi.invocationReward() > 0) { @@ -706,9 +758,9 @@ struct NOST : public ContractBase } break; case EAuctionType::Standard: - if (!resolveStandardAuctionCreateParams(input.minimumPurchaseQuantity, input.minimumBidIncrementPerUnit, - locals.resolvedQuantityForSale, locals.resolvedMinimumPurchaseQuantity, - input.buyNowPricePerUnit, input.initialPricePerUnit, input.salePricePerUnit)) + if (!resolveStandardAuctionCreateParams(input.minimumPurchaseQuantity, input.minimumBidIncrement, locals.resolvedQuantityForSale, + locals.resolvedMinimumPurchaseQuantity, input.buyNowPrice, input.initialPrice, + input.salePrice)) { if (qpi.invocationReward() > 0) { @@ -776,10 +828,10 @@ struct NOST : public ContractBase locals.auction.auctionId = id::randomValue(); locals.auction.quantityForSale = locals.resolvedQuantityForSale; locals.auction.minimumPurchaseQuantity = locals.resolvedMinimumPurchaseQuantity; - locals.auction.initialPricePerUnit = input.initialPricePerUnit; - locals.auction.salePricePerUnit = input.salePricePerUnit; - locals.auction.minimumBidIncrement = input.minimumBidIncrementPerUnit; - locals.auction.buyNowPricePerUnit = input.buyNowPricePerUnit; + locals.auction.initialPrice = input.initialPrice; + locals.auction.salePrice = input.salePrice; + locals.auction.minimumBidIncrement = input.minimumBidIncrement; + locals.auction.buyNowPrice = input.buyNowPrice; locals.auction.auctionDurationSeconds = smul(static_cast(input.durationDays), NOST_SECONDS_PER_DAY); locals.auction.createdAt = qpi.now(); locals.auction.lastBidAt = locals.auction.createdAt; @@ -881,7 +933,7 @@ struct NOST : public ContractBase { locals.effectiveQuantity = locals.auction.quantityForSale; } - if (locals.effectiveQuantity == 0 || locals.effectiveQuantity < locals.auction.minimumPurchaseQuantity || input.pricePerUnit == 0) + if (locals.effectiveQuantity == 0 || locals.effectiveQuantity < locals.auction.minimumPurchaseQuantity || input.bidAmount == 0) { if (qpi.invocationReward() > 0) { @@ -892,7 +944,7 @@ struct NOST : public ContractBase if (locals.auction.type == EAuctionType::Batch) { - if (input.pricePerUnit < locals.auction.salePricePerUnit) + if (input.bidAmount < locals.auction.salePrice) { if (qpi.invocationReward() > 0) { @@ -904,9 +956,9 @@ struct NOST : public ContractBase } else { - if (locals.auction.highestBidPerUnit == 0) + if (locals.auction.highestBidPrice == 0) { - if (input.pricePerUnit < locals.auction.initialPricePerUnit) + if (input.bidAmount < locals.auction.initialPrice) { if (qpi.invocationReward() > 0) { @@ -918,7 +970,7 @@ struct NOST : public ContractBase } else { - if (input.pricePerUnit < sadd(locals.auction.highestBidPerUnit, locals.auction.minimumBidIncrement)) + if (input.bidAmount < sadd(locals.auction.highestBidPrice, locals.auction.minimumBidIncrement)) { if (qpi.invocationReward() > 0) { @@ -930,7 +982,7 @@ struct NOST : public ContractBase } } - locals.requiredEscrow = smul(locals.effectiveQuantity, input.pricePerUnit); + locals.requiredEscrow = input.bidAmount; if (static_cast(qpi.invocationReward()) < locals.requiredEscrow) { if (qpi.invocationReward() > 0) @@ -952,7 +1004,7 @@ struct NOST : public ContractBase locals.participantData.escrowedAmount = locals.requiredEscrow; locals.participantData.requestedQuantity = locals.effectiveQuantity; locals.participantData.allocatedQuantity = 0; - locals.participantData.pricePerUnit = input.pricePerUnit; + locals.participantData.bidAmount = input.bidAmount; locals.participantData.lastBidTime = locals.currentDate; locals.participantData.participant = qpi.invocator(); locals.participantData.isHighestBidder = 0; @@ -984,16 +1036,16 @@ struct NOST : public ContractBase locals.participantData.isHighestBidder = 1; locals.participantData.isWinningBid = 1; locals.auction.highestBidder = qpi.invocator(); - locals.auction.highestBidPerUnit = input.pricePerUnit; + locals.auction.highestBidPrice = input.bidAmount; locals.auction.highestBidQuantity = locals.effectiveQuantity; locals.auction.highestBidAmount = locals.requiredEscrow; } else { - if (input.pricePerUnit > locals.auction.highestBidPerUnit) + if (input.bidAmount > locals.auction.highestBidPrice) { locals.auction.highestBidder = qpi.invocator(); - locals.auction.highestBidPerUnit = input.pricePerUnit; + locals.auction.highestBidPrice = input.bidAmount; locals.auction.highestBidQuantity = locals.effectiveQuantity; locals.auction.highestBidAmount = locals.requiredEscrow; locals.participantData.isHighestBidder = 1; @@ -1005,7 +1057,7 @@ struct NOST : public ContractBase { locals.auction.auctionDurationSeconds = sadd(locals.auction.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); } - if (locals.auction.buyNowPricePerUnit > 0 && input.pricePerUnit >= locals.auction.buyNowPricePerUnit) + if (locals.auction.buyNowPrice > 0 && input.bidAmount >= locals.auction.buyNowPrice) { locals.auction.status = EAuctionStatus::Finalized; locals.auction.settledAt = locals.currentDate; @@ -1039,6 +1091,87 @@ struct NOST : public ContractBase output.errorCode = static_cast(EAuctionError::Success); } + PUBLIC_PROCEDURE_WITH_LOCALS(CancelAuction) + { + output.errorCode = static_cast(EAuctionError::InvalidInput); + + if (!state.get().auctionList.get(input.auctionId, locals.auction)) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::AuctionNotFound); + return; + } + + if (locals.auction.status != EAuctionStatus::Active) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::AuctionClosed); + return; + } + + if (locals.auction.seller != qpi.invocator()) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::Forbidden); + return; + } + + locals.cancellationBaseAmount = max(locals.auction.highestBidAmount, locals.auction.salePrice); + output.cancellationFee = div(smul(locals.cancellationBaseAmount, state.get().auctionCancellationFeeBasisPoints), 10000ULL); + + if (static_cast(qpi.invocationReward()) < output.cancellationFee) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::InsufficientFunds); + return; + } + + locals.participantIndex = state.get().participants.nextElementIndex(NULL_INDEX); + while (locals.participantIndex != NULL_INDEX) + { + locals.participantKey = state.get().participants.key(locals.participantIndex); + if (locals.participantKey.auctionId == input.auctionId) + { + locals.participantData = state.get().participants.value(locals.participantIndex); + if (locals.participantData.escrowedAmount > 0) + { + qpi.transfer(locals.participantData.participant, locals.participantData.escrowedAmount); + output.refundedAmount = sadd(output.refundedAmount, locals.participantData.escrowedAmount); + } + state.mut().participants.removeByKey(locals.participantKey); + } + + locals.participantIndex = state.get().participants.nextElementIndex(locals.participantIndex); + } + + locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.auctionLotItems; + CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); + + locals.currentDate = qpi.now(); + locals.auction.status = EAuctionStatus::Cancelled; + locals.auction.settledAt = locals.currentDate; + state.mut().auctionList.replace(input.auctionId, locals.auction); + + if (static_cast(qpi.invocationReward()) > output.cancellationFee) + { + qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - output.cancellationFee); + } + + output.errorCode = static_cast(EAuctionError::Success); + } + PUBLIC_FUNCTION(GetAuction) { state.get().auctionList.get(input.auctionId, output.auction); } PUBLIC_FUNCTION(GetAuctionParticipant) @@ -1079,12 +1212,22 @@ struct NOST : public ContractBase } protected: + template + static constexpr T min(const T& a, const T& b) + { + return (a < b) ? a : b; + } + template + static constexpr T max(const T& a, const T& b) + { + return a > b ? a : b; + } static bool resolveBatchAuctionCreateParams(uint64 lotItemCount, uint64 totalEscrowQuantity, uint64 minimumPurchaseQuantity, - uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPricePerUnit) + uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPrice) { quantityForSale = 0; resolvedMinimumPurchaseQuantity = 0; - if (lotItemCount != 1 || minimumPurchaseQuantity == 0 || minimumPurchaseQuantity > totalEscrowQuantity || buyNowPricePerUnit != 0) + if (lotItemCount != 1 || minimumPurchaseQuantity == 0 || minimumPurchaseQuantity > totalEscrowQuantity || buyNowPrice != 0) { return false; } @@ -1093,23 +1236,22 @@ struct NOST : public ContractBase return true; } - static bool resolveStandardAuctionCreateParams(uint64 minimumPurchaseQuantity, uint64 minimumBidIncrementPerUnit, uint64& quantityForSale, - uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPricePerUnit, uint64 initialPricePerUnit, - uint64 salePricePerUnit) + static bool resolveStandardAuctionCreateParams(uint64 minimumPurchaseQuantity, uint64 minimumBidIncrement, uint64& quantityForSale, + uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPrice, uint64 initialPrice, uint64 salePrice) { quantityForSale = 0; resolvedMinimumPurchaseQuantity = 0; - if (minimumPurchaseQuantity > 1 || minimumBidIncrementPerUnit == 0 || salePricePerUnit == 0) + if (minimumPurchaseQuantity > 1 || minimumBidIncrement == 0 || salePrice == 0) { return false; } - if (initialPricePerUnit > salePricePerUnit) + if (initialPrice > salePrice) { return false; } - if (buyNowPricePerUnit > 0 && (buyNowPricePerUnit < initialPricePerUnit || buyNowPricePerUnit < salePricePerUnit)) + if (buyNowPrice > 0 && (buyNowPrice < initialPrice || buyNowPrice < salePrice)) { return false; From f2c31f4d8a089a29e335ef7798c63945563df18f Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 15 Apr 2026 18:03:05 +0300 Subject: [PATCH 09/59] Expand access asset logic in `Nostromo`: replace single asset with multiple required assets, add validation functions, and refactor private auction checks. --- src/contracts/Nostromo.h | 139 +++++++++++++++++++++++++++++++-------- 1 file changed, 110 insertions(+), 29 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 7eda84085..14f515713 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -1,10 +1,19 @@ using namespace QPI; +namespace QPI +{ + inline bool operator==(const Asset& lhs, const Asset& rhs) + { + return lhs.assetName == rhs.assetName && lhs.issuer == rhs.issuer; + } +} // namespace QPI + constexpr uint64 NOST_AUCTION_NUM = 2048; constexpr uint64 NOST_AUCTION_METADATA_CID_LENGTH = 64; constexpr uint64 NOST_AUCTION_PARTICIPANT_NUM = 4096; constexpr uint64 NOST_AUCTION_LOT_ITEM_NUM = 64; constexpr uint64 NOST_AUCTION_ALLOWED_WALLET_NUM = 128; +constexpr uint64 NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM = 16; constexpr uint32 NOST_AUCTION_MAX_DURATION_DAYS = 30; constexpr sint64 NOST_PRIVATE_AUCTION_FEE = 50000000LL; constexpr uint64 NOST_AUCTION_CANCELLATION_FEE_BP = 1000ULL; @@ -62,18 +71,7 @@ struct NOST : public ContractBase id auctionId; id participant; - bool operator<(const AuctionParticipantKey& rhs) const - { - if (auctionId < rhs.auctionId) - { - return true; - } - if (rhs.auctionId < auctionId) - { - return false; - } - return participant < rhs.participant; - } + bool operator==(const AuctionParticipantKey& rhs) const { return auctionId == rhs.auctionId && participant == rhs.participant; } }; /** @@ -187,8 +185,8 @@ struct NOST : public ContractBase /// Wallet that currently holds the highest bid. id highestBidder; - /// Asset required for participation when the auction visibility is private. - Asset requiredAccessAsset; + /// Asset set required for participation when the auction visibility is private and asset-based access is used. + HashSet requiredAccessAssets; /// Auction lot contents; one non-empty entry means a single asset, multiple non-empty entries mean a bundle. Array auctionLotItems; @@ -232,8 +230,8 @@ struct NOST : public ContractBase /// Auction lot contents; one non-empty entry means a single asset, multiple non-empty entries mean a bundle. Array auctionLotItems; - /// Asset required to participate when the auction is configured as private and asset-based access is used. - Asset requiredAccessAsset; + /// Asset list required to participate when the auction is configured as private and asset-based access is used. + Array requiredAccessAssets; /// Wallet list for private batch auctions; copied into the auction whitelist on creation. Array allowedBidderWallets; @@ -356,6 +354,39 @@ struct NOST : public ContractBase uint64 allowedWalletIndex; }; + struct CountRequiredAccessAssets_input + { + Array requiredAccessAssets; + }; + + struct CountRequiredAccessAssets_output + { + uint64 requiredAccessAssetCount; + }; + + struct CountRequiredAccessAssets_locals + { + Asset requiredAccessAsset; + uint64 requiredAccessAssetIndex; + }; + + struct HasRequiredAccessAsset_input + { + AuctionData auction; + }; + + struct HasRequiredAccessAsset_output + { + uint8 hasRequiredAccessAsset; + }; + + struct HasRequiredAccessAsset_locals + { + Asset requiredAccessAsset; + sint64 requiredAccessAssetSetIndex; + sint64 possessedAccessShares; + }; + struct ValidateMetadataCid_input { Array metadataIpfsCid; @@ -431,6 +462,8 @@ struct NOST : public ContractBase AnalyzeAuctionLot_output analyzeAuctionLotOutput; CountAllowedBidderWallets_input countAllowedBidderWalletsInput; CountAllowedBidderWallets_output countAllowedBidderWalletsOutput; + CountRequiredAccessAssets_input countRequiredAccessAssetsInput; + CountRequiredAccessAssets_output countRequiredAccessAssetsOutput; VerifyAuctionLotBalances_input verifyAuctionLotBalancesInput; EscrowAuctionLotAssets_input escrowAuctionLotAssetsInput; RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; @@ -438,6 +471,7 @@ struct NOST : public ContractBase uint64 resolvedQuantityForSale; uint64 resolvedMinimumPurchaseQuantity; uint64 allowedWalletIndex; + uint64 requiredAccessAssetIndex; RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; EscrowAuctionLotAssets_output escrowAuctionLotAssetsOutput; VerifyAuctionLotBalances_output verifyAuctionLotBalancesOutput; @@ -450,6 +484,8 @@ struct NOST : public ContractBase AuctionParticipantData previousHighestBidderData; AuctionParticipantKey participantKey; AuctionParticipantKey highestBidderKey; + HasRequiredAccessAsset_input hasRequiredAccessAssetInput; + HasRequiredAccessAsset_output hasRequiredAccessAssetOutput; uint64 elapsedSeconds; uint64 requiredEscrow; uint64 previousEscrow; @@ -571,6 +607,38 @@ struct NOST : public ContractBase } } + PRIVATE_FUNCTION_WITH_LOCALS(CountRequiredAccessAssets) + { + output.requiredAccessAssetCount = 0; + for (locals.requiredAccessAssetIndex = 0; locals.requiredAccessAssetIndex < input.requiredAccessAssets.capacity(); + ++locals.requiredAccessAssetIndex) + { + locals.requiredAccessAsset = input.requiredAccessAssets.get(locals.requiredAccessAssetIndex); + if (!isZeroAsset(locals.requiredAccessAsset)) + { + output.requiredAccessAssetCount = sadd(output.requiredAccessAssetCount, 1ULL); + } + } + } + + PRIVATE_FUNCTION_WITH_LOCALS(HasRequiredAccessAsset) + { + output.hasRequiredAccessAsset = 0; + for (locals.requiredAccessAssetSetIndex = input.auction.requiredAccessAssets.nextElementIndex(NULL_INDEX); + locals.requiredAccessAssetSetIndex != NULL_INDEX; + locals.requiredAccessAssetSetIndex = input.auction.requiredAccessAssets.nextElementIndex(locals.requiredAccessAssetSetIndex)) + { + locals.requiredAccessAsset = input.auction.requiredAccessAssets.key(locals.requiredAccessAssetSetIndex); + locals.possessedAccessShares = qpi.numberOfShares(locals.requiredAccessAsset, AssetOwnershipSelect::byOwner(qpi.invocator()), + AssetPossessionSelect::byPossessor(qpi.invocator())); + if (locals.possessedAccessShares > 0) + { + output.hasRequiredAccessAsset = 1; + return; + } + } + } + PRIVATE_FUNCTION_WITH_LOCALS(ValidateMetadataCid) { output.isValid = 0; @@ -780,7 +848,10 @@ struct NOST : public ContractBase locals.countAllowedBidderWalletsInput.allowedBidderWallets = input.allowedBidderWallets; CALL(CountAllowedBidderWallets, locals.countAllowedBidderWalletsInput, locals.countAllowedBidderWalletsOutput); - if (!validatePrivateAuctionAccess(static_cast(input.auctionVisibility), input.requiredAccessAsset, + locals.countRequiredAccessAssetsInput.requiredAccessAssets = input.requiredAccessAssets; + CALL(CountRequiredAccessAssets, locals.countRequiredAccessAssetsInput, locals.countRequiredAccessAssetsOutput); + if (!validatePrivateAuctionAccess(static_cast(input.auctionVisibility), + locals.countRequiredAccessAssetsOutput.requiredAccessAssetCount, locals.countAllowedBidderWalletsOutput.allowedWalletCount)) { if (qpi.invocationReward() > 0) @@ -836,7 +907,14 @@ struct NOST : public ContractBase locals.auction.createdAt = qpi.now(); locals.auction.lastBidAt = locals.auction.createdAt; locals.auction.seller = qpi.invocator(); - locals.auction.requiredAccessAsset = input.requiredAccessAsset; + for (locals.requiredAccessAssetIndex = 0; locals.requiredAccessAssetIndex < input.requiredAccessAssets.capacity(); + ++locals.requiredAccessAssetIndex) + { + if (!isZeroAsset(input.requiredAccessAssets.get(locals.requiredAccessAssetIndex))) + { + locals.auction.requiredAccessAssets.add(input.requiredAccessAssets.get(locals.requiredAccessAssetIndex)); + } + } locals.auction.auctionLotItems = input.auctionLotItems; for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < NOST_AUCTION_ALLOWED_WALLET_NUM; ++locals.allowedWalletIndex) { @@ -862,7 +940,7 @@ struct NOST : public ContractBase return; } - if (static_cast(qpi.invocationReward()) > locals.requiredFee) + if (qpi.invocationReward() > locals.requiredFee) { qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - locals.requiredFee); } @@ -915,17 +993,20 @@ struct NOST : public ContractBase return; } - if (locals.auction.visibility == EAuctionVisibility::Private && !locals.auction.allowedBidderWallets.contains(qpi.invocator()) && - (isZeroAsset(locals.auction.requiredAccessAsset) || - qpi.numberOfShares(locals.auction.requiredAccessAsset, AssetOwnershipSelect::byOwner(qpi.invocator()), - AssetPossessionSelect::byPossessor(qpi.invocator())) <= 0)) + if (locals.auction.visibility == EAuctionVisibility::Private && !locals.auction.allowedBidderWallets.contains(qpi.invocator())) { - if (qpi.invocationReward() > 0) + locals.hasRequiredAccessAssetInput.auction = locals.auction; + CALL(HasRequiredAccessAsset, locals.hasRequiredAccessAssetInput, locals.hasRequiredAccessAssetOutput); + + if (!locals.hasRequiredAccessAssetOutput.hasRequiredAccessAsset) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::PrivateAuctionAccessDenied); + return; } - output.errorCode = static_cast(EAuctionError::PrivateAuctionAccessDenied); - return; } locals.effectiveQuantity = input.quantity; @@ -1262,9 +1343,9 @@ struct NOST : public ContractBase return true; } - static bool validatePrivateAuctionAccess(EAuctionVisibility visibility, const Asset& requiredAccessAsset, uint64 allowedWalletCount) + static bool validatePrivateAuctionAccess(EAuctionVisibility visibility, uint64 requiredAccessAssetCount, uint64 allowedWalletCount) { - return visibility != EAuctionVisibility::Private || !isZeroAsset(requiredAccessAsset) || allowedWalletCount > 0; + return visibility != EAuctionVisibility::Private || requiredAccessAssetCount > 0 || allowedWalletCount > 0; } static sint64 getCreateAuctionFee(EAuctionVisibility visibility, const ContractState& state) From e39a4e32ab2693ab4e37d63a2a4c965977275ad6 Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 15 Apr 2026 18:24:59 +0300 Subject: [PATCH 10/59] Refactor private auction access logic in `Nostromo`: replace boolean flags with `uint8`, enhance access validation with `hasAccess`, and refine checks for required access assets and allowed wallets. --- src/contracts/Nostromo.h | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 14f515713..18a6e8226 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -491,8 +491,9 @@ struct NOST : public ContractBase uint64 previousEscrow; uint64 effectiveQuantity; DateAndTime currentDate; - bool participantExists; - bool highestBidderExists; + uint8 participantExists; + uint8 highestBidderExists; + uint8 hasAccess; }; struct CancelAuction_locals @@ -962,6 +963,7 @@ struct NOST : public ContractBase output.errorCode = static_cast(EAuctionError::AuctionNotFound); return; } + if (locals.auction.status != EAuctionStatus::Active) { if (qpi.invocationReward() > 0) @@ -971,6 +973,7 @@ struct NOST : public ContractBase output.errorCode = static_cast(EAuctionError::AuctionClosed); return; } + if (locals.auction.seller == qpi.invocator()) { if (qpi.invocationReward() > 0) @@ -993,12 +996,20 @@ struct NOST : public ContractBase return; } - if (locals.auction.visibility == EAuctionVisibility::Private && !locals.auction.allowedBidderWallets.contains(qpi.invocator())) + if (locals.auction.visibility == EAuctionVisibility::Private) { - locals.hasRequiredAccessAssetInput.auction = locals.auction; - CALL(HasRequiredAccessAsset, locals.hasRequiredAccessAssetInput, locals.hasRequiredAccessAssetOutput); + if (locals.auction.requiredAccessAssets.population() > 0) + { + locals.hasRequiredAccessAssetInput.auction = locals.auction; + CALL(HasRequiredAccessAsset, locals.hasRequiredAccessAssetInput, locals.hasRequiredAccessAssetOutput); + locals.hasAccess = locals.hasRequiredAccessAssetOutput.hasRequiredAccessAsset; + } + else + { + locals.hasAccess = locals.auction.allowedBidderWallets.contains(qpi.invocator()); + } - if (!locals.hasRequiredAccessAssetOutput.hasRequiredAccessAsset) + if (!locals.hasAccess) { if (qpi.invocationReward() > 0) { @@ -1345,7 +1356,8 @@ struct NOST : public ContractBase static bool validatePrivateAuctionAccess(EAuctionVisibility visibility, uint64 requiredAccessAssetCount, uint64 allowedWalletCount) { - return visibility != EAuctionVisibility::Private || requiredAccessAssetCount > 0 || allowedWalletCount > 0; + return visibility != EAuctionVisibility::Private || + ((requiredAccessAssetCount > 0) != (allowedWalletCount > 0)); } static sint64 getCreateAuctionFee(EAuctionVisibility visibility, const ContractState& state) From 321a2f27c44c14b5005bcb3f9f4ed75695491164 Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 15 Apr 2026 18:40:36 +0300 Subject: [PATCH 11/59] Modularize bid processing in `Nostromo`: introduce `ProcessBatchBid` and `ProcessStandardBid` procedures, refactor `PlaceBid` for auction-type-specific bid handling, and encapsulate participant and auction updates. --- src/contracts/Nostromo.h | 411 ++++++++++++++++++++++++++------------- 1 file changed, 273 insertions(+), 138 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 18a6e8226..c7b45b209 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -387,6 +387,60 @@ struct NOST : public ContractBase sint64 possessedAccessShares; }; + struct ProcessBatchBid_input + { + id auctionId; + uint64 effectiveQuantity; + uint64 bidAmount; + uint64 requiredEscrow; + DateAndTime currentDate; + uint64 elapsedSeconds; + }; + + struct ProcessBatchBid_output + { + uint64 refundedAmount; + uint8 errorCode; + uint8 success; + }; + + struct ProcessBatchBid_locals + { + AuctionData auction; + AuctionParticipantData participantData; + AuctionParticipantKey participantKey; + uint64 previousEscrow; + uint8 participantExists; + }; + + struct ProcessStandardBid_input + { + id auctionId; + uint64 bidAmount; + uint64 requiredEscrow; + DateAndTime currentDate; + uint64 elapsedSeconds; + }; + + struct ProcessStandardBid_output + { + uint64 refundedAmount; + uint8 errorCode; + uint8 success; + }; + + struct ProcessStandardBid_locals + { + AuctionData auction; + AuctionParticipantData participantData; + AuctionParticipantData previousHighestBidderData; + AuctionParticipantKey participantKey; + AuctionParticipantKey highestBidderKey; + uint64 previousEscrow; + uint8 participantExists; + uint8 highestBidderExists; + }; + struct ValidateMetadataCid_input { Array metadataIpfsCid; @@ -480,19 +534,15 @@ struct NOST : public ContractBase struct PlaceBid_locals { AuctionData auction; - AuctionParticipantData participantData; - AuctionParticipantData previousHighestBidderData; - AuctionParticipantKey participantKey; - AuctionParticipantKey highestBidderKey; HasRequiredAccessAsset_input hasRequiredAccessAssetInput; HasRequiredAccessAsset_output hasRequiredAccessAssetOutput; + ProcessBatchBid_input processBatchBidInput; + ProcessBatchBid_output processBatchBidOutput; + ProcessStandardBid_input processStandardBidInput; + ProcessStandardBid_output processStandardBidOutput; uint64 elapsedSeconds; uint64 requiredEscrow; - uint64 previousEscrow; - uint64 effectiveQuantity; DateAndTime currentDate; - uint8 participantExists; - uint8 highestBidderExists; uint8 hasAccess; }; @@ -640,6 +690,186 @@ struct NOST : public ContractBase } } + PRIVATE_PROCEDURE_WITH_LOCALS(ProcessBatchBid) + { + output.refundedAmount = 0; + output.errorCode = static_cast(EAuctionError::Success); + output.success = 0; + if (!state.get().auctionList.get(input.auctionId, locals.auction)) + { + output.errorCode = static_cast(EAuctionError::AuctionNotFound); + return; + } + + if (input.effectiveQuantity == 0 || input.effectiveQuantity < locals.auction.minimumPurchaseQuantity || input.bidAmount == 0) + { + output.errorCode = static_cast(EAuctionError::InvalidInput); + return; + } + + if (input.bidAmount < locals.auction.salePrice) + { + output.errorCode = static_cast(EAuctionError::BidTooLow); + return; + } + + locals.participantKey = {input.auctionId, qpi.invocator()}; + locals.participantExists = state.get().participants.get(locals.participantKey, locals.participantData); + locals.previousEscrow = locals.participantExists ? locals.participantData.escrowedAmount : 0; + + locals.participantData.escrowedAmount = input.requiredEscrow; + 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.isHighestBidder = 0; + locals.participantData.isWinningBid = 0; + + if (!locals.participantExists) + { + locals.auction.bidderCount = sadd(locals.auction.bidderCount, 1U); + } + + if (input.bidAmount > locals.auction.highestBidPrice) + { + locals.auction.highestBidder = qpi.invocator(); + locals.auction.highestBidPrice = input.bidAmount; + locals.auction.highestBidQuantity = input.effectiveQuantity; + locals.auction.highestBidAmount = input.requiredEscrow; + locals.participantData.isHighestBidder = 1; + } + + locals.auction.lastBidAt = input.currentDate; + if ((locals.auction.auctionDurationSeconds - input.elapsedSeconds) <= NOST_AUCTION_EXTENSION_SECONDS) + { + locals.auction.auctionDurationSeconds = sadd(locals.auction.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); + } + if (locals.auction.buyNowPrice > 0 && input.bidAmount >= locals.auction.buyNowPrice) + { + locals.auction.status = EAuctionStatus::Finalized; + locals.auction.settledAt = input.currentDate; + locals.participantData.isWinningBid = 1; + locals.participantData.isHighestBidder = 1; + } + + if (state.mut().participants.set(locals.participantKey, locals.participantData) == NULL_INDEX) + { + output.errorCode = static_cast(EAuctionError::StorageFull); + return; + } + state.mut().auctionList.replace(input.auctionId, locals.auction); + + if (locals.previousEscrow > 0) + { + qpi.transfer(qpi.invocator(), locals.previousEscrow); + output.refundedAmount = sadd(output.refundedAmount, locals.previousEscrow); + } + + output.success = 1; + } + + PRIVATE_PROCEDURE_WITH_LOCALS(ProcessStandardBid) + { + output.refundedAmount = 0; + output.errorCode = static_cast(EAuctionError::Success); + output.success = 0; + locals.highestBidderExists = 0; + if (!state.get().auctionList.get(input.auctionId, locals.auction)) + { + output.errorCode = static_cast(EAuctionError::AuctionNotFound); + return; + } + + if (locals.auction.quantityForSale == 0 || locals.auction.quantityForSale < locals.auction.minimumPurchaseQuantity || input.bidAmount == 0) + { + output.errorCode = static_cast(EAuctionError::InvalidInput); + return; + } + + if (locals.auction.highestBidPrice == 0) + { + if (input.bidAmount < locals.auction.initialPrice) + { + output.errorCode = static_cast(EAuctionError::BidTooLow); + return; + } + } + else if (input.bidAmount < sadd(locals.auction.highestBidPrice, locals.auction.minimumBidIncrement)) + { + output.errorCode = static_cast(EAuctionError::BidTooLow); + return; + } + + locals.participantKey = {input.auctionId, qpi.invocator()}; + locals.participantExists = state.get().participants.get(locals.participantKey, locals.participantData); + locals.previousEscrow = locals.participantExists ? locals.participantData.escrowedAmount : 0; + + locals.participantData.escrowedAmount = input.requiredEscrow; + locals.participantData.requestedQuantity = locals.auction.quantityForSale; + locals.participantData.allocatedQuantity = 0; + locals.participantData.bidAmount = input.bidAmount; + locals.participantData.lastBidTime = input.currentDate; + locals.participantData.participant = qpi.invocator(); + locals.participantData.isHighestBidder = 0; + locals.participantData.isWinningBid = 0; + + if (!locals.participantExists) + { + locals.auction.bidderCount = sadd(locals.auction.bidderCount, 1U); + } + + if (!isZero(locals.auction.highestBidder)) + { + locals.highestBidderKey = {locals.auction.auctionId, locals.auction.highestBidder}; + locals.highestBidderExists = state.get().participants.get(locals.highestBidderKey, locals.previousHighestBidderData); + } + if (locals.highestBidderExists && locals.previousHighestBidderData.participant != qpi.invocator()) + { + qpi.transfer(locals.previousHighestBidderData.participant, locals.previousHighestBidderData.escrowedAmount); + output.refundedAmount = sadd(output.refundedAmount, locals.previousHighestBidderData.escrowedAmount); + locals.previousHighestBidderData.escrowedAmount = 0; + locals.previousHighestBidderData.isHighestBidder = 0; + locals.previousHighestBidderData.isWinningBid = 0; + state.mut().participants.replace(locals.highestBidderKey, locals.previousHighestBidderData); + } + + locals.participantData.isHighestBidder = 1; + locals.participantData.isWinningBid = 1; + locals.auction.highestBidder = qpi.invocator(); + locals.auction.highestBidPrice = input.bidAmount; + locals.auction.highestBidQuantity = locals.auction.quantityForSale; + locals.auction.highestBidAmount = input.requiredEscrow; + + locals.auction.lastBidAt = input.currentDate; + if ((locals.auction.auctionDurationSeconds - input.elapsedSeconds) <= NOST_AUCTION_EXTENSION_SECONDS) + { + locals.auction.auctionDurationSeconds = sadd(locals.auction.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); + } + if (locals.auction.buyNowPrice > 0 && input.bidAmount >= locals.auction.buyNowPrice) + { + locals.auction.status = EAuctionStatus::Finalized; + locals.auction.settledAt = input.currentDate; + locals.participantData.isWinningBid = 1; + locals.participantData.isHighestBidder = 1; + } + + if (state.mut().participants.set(locals.participantKey, locals.participantData) == NULL_INDEX) + { + output.errorCode = static_cast(EAuctionError::StorageFull); + return; + } + state.mut().auctionList.replace(input.auctionId, locals.auction); + + if (locals.previousEscrow > 0) + { + qpi.transfer(qpi.invocator(), locals.previousEscrow); + output.refundedAmount = sadd(output.refundedAmount, locals.previousEscrow); + } + + output.success = 1; + } + PRIVATE_FUNCTION_WITH_LOCALS(ValidateMetadataCid) { output.isValid = 0; @@ -1020,159 +1250,65 @@ struct NOST : public ContractBase } } - locals.effectiveQuantity = input.quantity; - if (locals.auction.type == EAuctionType::Standard) - { - locals.effectiveQuantity = locals.auction.quantityForSale; - } - if (locals.effectiveQuantity == 0 || locals.effectiveQuantity < locals.auction.minimumPurchaseQuantity || input.bidAmount == 0) + locals.requiredEscrow = input.bidAmount; + if (static_cast(qpi.invocationReward()) < locals.requiredEscrow) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + output.errorCode = static_cast(EAuctionError::InsufficientFunds); return; } - if (locals.auction.type == EAuctionType::Batch) - { - if (input.bidAmount < locals.auction.salePrice) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = static_cast(EAuctionError::BidTooLow); - return; - } - } - else + switch (locals.auction.type) { - if (locals.auction.highestBidPrice == 0) - { - if (input.bidAmount < locals.auction.initialPrice) + case EAuctionType::Batch: + locals.processBatchBidInput.auctionId = input.auctionId; + locals.processBatchBidInput.effectiveQuantity = input.quantity; + locals.processBatchBidInput.bidAmount = input.bidAmount; + locals.processBatchBidInput.requiredEscrow = locals.requiredEscrow; + 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.errorCode = static_cast(EAuctionError::BidTooLow); + output.errorCode = locals.processBatchBidOutput.errorCode; return; } - } - else - { - if (input.bidAmount < sadd(locals.auction.highestBidPrice, locals.auction.minimumBidIncrement)) + output.refundedAmount = sadd(output.refundedAmount, locals.processBatchBidOutput.refundedAmount); + break; + case EAuctionType::Standard: + locals.processStandardBidInput.auctionId = input.auctionId; + locals.processStandardBidInput.bidAmount = input.bidAmount; + locals.processStandardBidInput.requiredEscrow = locals.requiredEscrow; + 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 = static_cast(EAuctionError::BidTooLow); + output.errorCode = locals.processStandardBidOutput.errorCode; return; } - } - } - - locals.requiredEscrow = input.bidAmount; - if (static_cast(qpi.invocationReward()) < locals.requiredEscrow) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = static_cast(EAuctionError::InsufficientFunds); - return; - } - - locals.participantKey = {input.auctionId, qpi.invocator()}; - locals.participantExists = state.get().participants.get(locals.participantKey, locals.participantData); - locals.previousEscrow = 0; - if (locals.participantExists) - { - locals.previousEscrow = locals.participantData.escrowedAmount; - } - - locals.participantData.escrowedAmount = locals.requiredEscrow; - locals.participantData.requestedQuantity = locals.effectiveQuantity; - locals.participantData.allocatedQuantity = 0; - locals.participantData.bidAmount = input.bidAmount; - locals.participantData.lastBidTime = locals.currentDate; - locals.participantData.participant = qpi.invocator(); - locals.participantData.isHighestBidder = 0; - locals.participantData.isWinningBid = 0; - - if (!locals.participantExists) - { - locals.auction.bidderCount = sadd(locals.auction.bidderCount, 1U); - } - - if (locals.auction.type == EAuctionType::Standard) - { - locals.highestBidderExists = false; - if (!isZero(locals.auction.highestBidder)) - { - locals.highestBidderKey = {input.auctionId, locals.auction.highestBidder}; - locals.highestBidderExists = state.get().participants.get(locals.highestBidderKey, locals.previousHighestBidderData); - } - if (locals.highestBidderExists && locals.previousHighestBidderData.participant != qpi.invocator()) - { - qpi.transfer(locals.previousHighestBidderData.participant, locals.previousHighestBidderData.escrowedAmount); - output.refundedAmount = sadd(output.refundedAmount, locals.previousHighestBidderData.escrowedAmount); - locals.previousHighestBidderData.escrowedAmount = 0; - locals.previousHighestBidderData.isHighestBidder = 0; - locals.previousHighestBidderData.isWinningBid = 0; - state.mut().participants.replace(locals.highestBidderKey, locals.previousHighestBidderData); - } - - locals.participantData.isHighestBidder = 1; - locals.participantData.isWinningBid = 1; - locals.auction.highestBidder = qpi.invocator(); - locals.auction.highestBidPrice = input.bidAmount; - locals.auction.highestBidQuantity = locals.effectiveQuantity; - locals.auction.highestBidAmount = locals.requiredEscrow; - } - else - { - if (input.bidAmount > locals.auction.highestBidPrice) - { - locals.auction.highestBidder = qpi.invocator(); - locals.auction.highestBidPrice = input.bidAmount; - locals.auction.highestBidQuantity = locals.effectiveQuantity; - locals.auction.highestBidAmount = locals.requiredEscrow; - locals.participantData.isHighestBidder = 1; - } - } - - locals.auction.lastBidAt = locals.currentDate; - if ((locals.auction.auctionDurationSeconds - locals.elapsedSeconds) <= NOST_AUCTION_EXTENSION_SECONDS) - { - locals.auction.auctionDurationSeconds = sadd(locals.auction.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); - } - if (locals.auction.buyNowPrice > 0 && input.bidAmount >= locals.auction.buyNowPrice) - { - locals.auction.status = EAuctionStatus::Finalized; - locals.auction.settledAt = locals.currentDate; - locals.participantData.isWinningBid = 1; - locals.participantData.isHighestBidder = 1; - } - - if (state.mut().participants.set(locals.participantKey, locals.participantData) == NULL_INDEX) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = static_cast(EAuctionError::StorageFull); - return; + output.refundedAmount = sadd(output.refundedAmount, locals.processStandardBidOutput.refundedAmount); + break; + default: + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::InvalidAuctionType); + return; } - state.mut().auctionList.replace(input.auctionId, locals.auction); - if (locals.previousEscrow > 0) - { - qpi.transfer(qpi.invocator(), locals.previousEscrow); - output.refundedAmount = sadd(output.refundedAmount, locals.previousEscrow); - } if (static_cast(qpi.invocationReward()) > locals.requiredEscrow) { qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - locals.requiredEscrow); @@ -1356,8 +1492,7 @@ struct NOST : public ContractBase static bool validatePrivateAuctionAccess(EAuctionVisibility visibility, uint64 requiredAccessAssetCount, uint64 allowedWalletCount) { - return visibility != EAuctionVisibility::Private || - ((requiredAccessAssetCount > 0) != (allowedWalletCount > 0)); + return visibility != EAuctionVisibility::Private || ((requiredAccessAssetCount > 0) != (allowedWalletCount > 0)); } static sint64 getCreateAuctionFee(EAuctionVisibility visibility, const ContractState& state) From 61e1cbd940ba31f49d8f76a77076d1a9fabc5f57 Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 15 Apr 2026 18:46:12 +0300 Subject: [PATCH 12/59] Add `@brief` documentation across `Nostromo` contract: enhance struct clarity with detailed field descriptions, standardize comment format, and improve internal documentation consistency. --- src/contracts/Nostromo.h | 220 +++++++++++++++++++++++++++++---------- 1 file changed, 165 insertions(+), 55 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index c7b45b209..bc07d4189 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -80,28 +80,28 @@ struct NOST : public ContractBase */ struct AuctionParticipantData { - /// Amount currently locked in escrow for this participant bid. + /** @brief Amount currently locked in escrow for the participant bid. */ uint64 escrowedAmount; - /// Quantity requested by this participant; standard auctions always use the whole lot quantity. + /** @brief Quantity requested by the participant; standard auctions always use the whole lot quantity. */ uint64 requestedQuantity; - /// Quantity finally allocated to this participant after batch settlement. + /** @brief Quantity finally allocated to the participant after batch auction settlement. */ uint64 allocatedQuantity; - /// Total bid amount committed by this participant for the current request. + /** @brief Total amount committed by the participant for the current bid. */ uint64 bidAmount; - /// Timestamp of the participant's latest accepted bid. + /** @brief Timestamp of the participant's latest accepted bid. */ DateAndTime lastBidTime; - /// Wallet that owns this participant record. + /** @brief Wallet that owns this participant record. */ id participant; - /// Marks the participant that currently holds the leading bid. + /** @brief Marks the participant that currently holds the leading bid. */ uint8 isHighestBidder; - /// Marks bids that remain inside the winning allocation after settlement. + /** @brief Marks bids that remain inside the winning allocation after settlement. */ uint8 isWinningBid; }; @@ -112,10 +112,10 @@ struct NOST : public ContractBase */ struct AuctionLotEntry { - /// Asset included in the lot. + /** @brief Asset included in the auction lot. */ Asset asset; - /// Quantity of this asset included in the lot. + /** @brief Quantity of this asset included in the auction lot. */ sint64 quantity; }; @@ -127,209 +127,250 @@ struct NOST : public ContractBase */ struct AuctionData { - /// Unique identifier of the auction created in the Auction House. + /** @brief Unique identifier of the auction created in the Auction House. */ id auctionId; - /// Total sale units offered in this auction; batch auctions use asset quantity, standard auctions use one unit for the whole lot. + /** @brief Total sale units offered; batch auctions use asset quantity, standard auctions use one unit for the whole lot. */ uint64 quantityForSale; - /// Quantity already assigned to winning bids after settlement. + /** @brief Quantity already assigned to winning bids after settlement. */ uint64 allocatedQuantity; - /// Minimum quantity a bidder may request in a batch auction; standard auctions sell the entire lot as one unit. + /** @brief Minimum quantity a bidder may request in a batch auction; standard auctions always sell the whole lot as one unit. */ uint64 minimumPurchaseQuantity; - /// Initial Price for a standard auction; bids cannot start below this total auction price. + /** @brief Initial price for a standard auction; bids cannot start below this total price for the whole lot. */ uint64 initialPrice; - /// Sale Price defined by the seller as the desired / minimum acceptable total selling price. + /** @brief Sale price defined by the seller as the desired or minimum acceptable total selling price. */ uint64 salePrice; - /// Minimum step by which a new bid must exceed the current highest bid. + /** @brief Minimum increment by which a new standard auction bid must exceed the current highest bid. */ uint64 minimumBidIncrement; - /// Buy Now price that closes a standard auction immediately when matched or exceeded. + /** @brief Buy Now price that closes a standard auction immediately when matched or exceeded. */ uint64 buyNowPrice; - /// Highest total bid currently offered by any active bid. + /** @brief Highest total bid currently offered by any active bid. */ uint64 highestBidPrice; - /// Quantity requested by the current highest bid. + /** @brief Quantity requested by the current highest bid. */ uint64 highestBidQuantity; - /// Total amount escrowed by the current highest bid. - /// Equal to the committed highest bid amount. + /** @brief Total amount escrowed by the current highest bid; equal to the committed highest bid amount. */ uint64 highestBidAmount; - /// Auction duration in seconds, derived from the duration configured in days. + /** @brief Auction duration in seconds, derived from the duration configured in days. */ uint64 auctionDurationSeconds; - /// Timestamp when the seller created the auction. + /** @brief Timestamp when the seller created the auction. */ DateAndTime createdAt; - /// Timestamp of the most recent accepted bid. + /** @brief Timestamp of the most recent accepted bid. */ DateAndTime lastBidAt; - /// Deadline for the seller to manually accept or reject a bid after auction end when the highest bid is between Initial Price and Sale Price. + /** @brief Deadline for the seller to accept or reject a standard auction bid that ended between Initial Price and Sale Price. */ DateAndTime sellerDecisionDeadline; - /// Timestamp when the auction was finalized, cancelled, or otherwise settled. + /** @brief Timestamp when the auction was finalized, cancelled, or otherwise settled. */ DateAndTime settledAt; - /// Number of distinct bidders who have placed bids in this auction. + /** @brief Number of distinct bidders that have placed bids in this auction. */ uint32 bidderCount; - /// Wallet that created the auction and offers the asset for sale. + /** @brief Wallet that created the auction and offers the lot for sale. */ id seller; - /// Wallet that currently holds the highest bid. + /** @brief Wallet that currently holds the highest bid. */ id highestBidder; - /// Asset set required for participation when the auction visibility is private and asset-based access is used. + /** @brief Asset set required for participation when the private auction uses asset-based access. */ HashSet requiredAccessAssets; - /// Auction lot contents; one non-empty entry means a single asset, multiple non-empty entries mean a bundle. + /** @brief Auction lot contents; one non-empty entry means a single asset, multiple non-empty entries mean a bundle. */ Array auctionLotItems; - /// Wallet whitelist for private batch auctions; only these wallets may participate when wallet-based restriction is used. + /** @brief Wallet whitelist used when the private auction uses wallet-based access. */ HashSet allowedBidderWallets; - /// IPFS CID stored in Pinata that points to the auction name and description metadata. + /** @brief Lowercase base32 CIDv1 stored in Pinata for the auction name and description metadata. */ Array metadataIpfsCid; - /// Auction House mode: Batch Auction or Standard Auction. + /** @brief Auction House mode: Batch Auction or Standard Auction. */ EAuctionType type; - /// Auction visibility: public or restricted private access. + /** @brief Auction visibility: public or restricted private access. */ EAuctionVisibility visibility; - /// Current lifecycle status of the auction, including the manual seller-decision phase. + /** @brief Current lifecycle status of the auction, including the seller decision phase for standard auctions. */ EAuctionStatus status; }; struct StateData { - /// Configured fee charged when creating a private auction. + /** @brief Configured fee charged when creating a private auction. */ sint64 privateAuctionFee; - /// Configured cancellation fee rate in basis points. + /** @brief Configured cancellation fee rate in basis points. */ uint64 auctionCancellationFeeBasisPoints; - /// Configured maximum auction duration in days. + /** @brief Configured maximum auction duration in days. */ uint32 maxAuctionDurationDays; HashMap auctionList; HashMap participants; }; + /** @brief Input payload used to create a Batch Auction or Standard Auction in the Auction House. */ struct CreateAuction_input { - /// IPFS CID stored in Pinata that points to the auction name and description metadata. + /** @brief Lowercase base32 CIDv1 stored in Pinata for the auction name and description metadata. */ Array metadataIpfsCid; - /// Auction lot contents; one non-empty entry means a single asset, multiple non-empty entries mean a bundle. + /** @brief Auction lot contents; one non-empty entry means a single asset, multiple non-empty entries mean a bundle. */ Array auctionLotItems; - /// Asset list required to participate when the auction is configured as private and asset-based access is used. + /** @brief Asset list required to participate when the private auction uses asset-based access. */ Array requiredAccessAssets; - /// Wallet list for private batch auctions; copied into the auction whitelist on creation. + /** @brief Wallet list used when the private auction restricts participation to predefined wallets. */ Array allowedBidderWallets; - /// Minimum quantity a bidder may request in a batch auction; standard auctions sell the whole lot as one unit. + /** @brief Minimum quantity a bidder may request in a batch auction; standard auctions always sell the whole lot as one unit. */ uint64 minimumPurchaseQuantity; - /// Initial Price for a standard auction; bids cannot be placed below this total auction price. + /** @brief Initial price for a standard auction; bids cannot be placed below this total price for the whole lot. */ uint64 initialPrice; - /// Sale Price defined by the seller as the desired / minimum acceptable total selling price. + /** @brief Sale price defined by the seller as the desired or minimum acceptable total selling price. */ uint64 salePrice; - /// Minimum step by which each new bid must exceed the current highest bid. + /** @brief Minimum increment by which each new standard auction bid must exceed the current highest bid. */ uint64 minimumBidIncrement; - /// Buy Now price that immediately closes a standard auction once matched or exceeded. + /** @brief Buy Now price that immediately closes a standard auction once matched or exceeded. */ uint64 buyNowPrice; - /// Auction duration configured by the seller in days, capped by the contract configuration. + /** @brief Auction duration in days, capped by the contract configuration. */ uint32 durationDays; - /// Auction House mode selected by the seller: Batch Auction or Standard Auction. + /** @brief Auction House mode selected by the seller: Batch Auction or Standard Auction. */ uint8 auctionType; - /// Visibility selected by the seller: public or private. + /** @brief Visibility selected by the seller: public or private. */ uint8 auctionVisibility; }; + /** @brief Result of auction creation. */ struct CreateAuction_output { + /** @brief Identifier assigned to the new auction when creation succeeds. */ id auctionId; + + /** @brief Result code describing whether the auction creation succeeded. */ uint8 errorCode; }; + /** @brief Input payload used to place a bid in a Batch Auction or Standard Auction. */ struct PlaceBid_input { - /// Identifier of the target auction. + /** @brief Identifier of the target auction. */ id auctionId; - /// Requested quantity for a batch auction; ignored for a standard auction because the whole lot is sold as one unit. + /** @brief Requested quantity for a batch auction; ignored for a standard auction because the whole lot is sold as one unit. */ uint64 quantity; - /// Total amount the bidder commits for this bid. + /** @brief Total amount the bidder commits for this bid. */ uint64 bidAmount; }; + /** @brief Result of a bid placement request. */ struct PlaceBid_output { + /** @brief Amount that remains escrowed for the accepted bid. */ uint64 escrowedAmount; + + /** @brief Amount refunded to the bidder, including replaced escrow or invocation change. */ uint64 refundedAmount; + + /** @brief Result code describing whether the bid placement succeeded. */ uint8 errorCode; }; + /** @brief Input payload used to cancel an active auction. */ struct CancelAuction_input { + /** @brief Identifier of the auction that the seller wants to cancel. */ id auctionId; }; + /** @brief Result of an auction cancellation request. */ struct CancelAuction_output { + /** @brief Total amount refunded to bidders because of the cancellation. */ uint64 refundedAmount; + + /** @brief Cancellation fee charged to the seller according to the auction rules. */ uint64 cancellationFee; + + /** @brief Result code describing whether the cancellation succeeded. */ uint8 errorCode; }; + /** @brief Input payload used to fetch one auction from storage. */ struct GetAuction_input { + /** @brief Identifier of the auction to read. */ id auctionId; }; + /** @brief Auction data returned by the read-only auction getter. */ struct GetAuction_output { + /** @brief Persistent auction data stored for the requested auction. */ AuctionData auction; }; + /** @brief Input payload used to fetch one participant record from an auction. */ struct GetAuctionParticipant_input { + /** @brief Identifier of the auction that owns the participant record. */ id auctionId; + + /** @brief Wallet whose participant record should be returned. */ id participant; }; + /** @brief Participant data returned by the read-only participant getter. */ struct GetAuctionParticipant_output { + /** @brief Participant record for the requested wallet in the requested auction. */ AuctionParticipantData participantData; + + /** @brief Flag indicating whether the participant record exists. */ uint8 found; }; + /** @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; }; @@ -339,13 +380,17 @@ struct NOST : public ContractBase 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; }; @@ -354,13 +399,17 @@ struct NOST : public ContractBase uint64 allowedWalletIndex; }; + /** @brief Internal input used to count non-empty asset entries in a private asset access list. */ struct CountRequiredAccessAssets_input { + /** @brief Asset list provided for private asset-based access control. */ Array requiredAccessAssets; }; + /** @brief Internal output containing the number of non-empty private access assets. */ struct CountRequiredAccessAssets_output { + /** @brief Number of non-zero asset entries found in the private access list. */ uint64 requiredAccessAssetCount; }; @@ -370,13 +419,17 @@ struct NOST : public ContractBase uint64 requiredAccessAssetIndex; }; + /** @brief Internal input used to verify whether the invocator owns at least one required private access asset. */ struct HasRequiredAccessAsset_input { + /** @brief Auction whose private asset-based access rules should be evaluated. */ AuctionData auction; }; + /** @brief Internal output of the private asset access check. */ struct HasRequiredAccessAsset_output { + /** @brief Flag indicating whether the invocator owns at least one required access asset. */ uint8 hasRequiredAccessAsset; }; @@ -387,20 +440,38 @@ struct NOST : public ContractBase sint64 possessedAccessShares; }; + /** @brief Internal input used to process a batch auction bid after the common PlaceBid checks succeed. */ struct ProcessBatchBid_input { + /** @brief Identifier of the target batch auction. */ id auctionId; + + /** @brief Quantity requested by the bidder in the batch auction. */ uint64 effectiveQuantity; + + /** @brief Total amount the bidder commits for the batch bid. */ uint64 bidAmount; + + /** @brief Amount that must remain escrowed for the batch bid. */ uint64 requiredEscrow; + + /** @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 batch auction bid. */ struct ProcessBatchBid_output { + /** @brief Amount refunded during batch bid processing. */ uint64 refundedAmount; + + /** @brief Result code describing whether the batch bid processing succeeded. */ uint8 errorCode; + + /** @brief Flag indicating whether batch bid processing completed successfully. */ uint8 success; }; @@ -413,19 +484,35 @@ struct NOST : public ContractBase uint8 participantExists; }; + /** @brief Internal input used to process a standard auction bid after the common PlaceBid checks succeed. */ struct ProcessStandardBid_input { + /** @brief Identifier of the target standard auction. */ id auctionId; + + /** @brief Total amount the bidder commits for the standard auction lot. */ uint64 bidAmount; + + /** @brief Amount that must remain escrowed for the standard bid. */ uint64 requiredEscrow; + + /** @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 refunded during standard bid processing. */ uint64 refundedAmount; + + /** @brief Result code describing whether the standard bid processing succeeded. */ uint8 errorCode; + + /** @brief Flag indicating whether standard bid processing completed successfully. */ uint8 success; }; @@ -441,13 +528,17 @@ struct NOST : public ContractBase uint8 highestBidderExists; }; + /** @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; }; @@ -459,13 +550,17 @@ struct NOST : public ContractBase 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; }; @@ -476,13 +571,17 @@ struct NOST : public ContractBase 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; }; @@ -494,8 +593,10 @@ struct NOST : public ContractBase sint64 transferredShares; }; + /** @brief Internal input used to roll back an auction lot escrow attempt or to return the lot after cancellation. */ struct RollbackAuctionLotAssets_input { + /** @brief Auction lot that must be returned from contract escrow to the seller. */ Array auctionLotItems; }; @@ -558,14 +659,23 @@ struct NOST : public ContractBase sint64 participantIndex; }; + /** @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; }; From e433ea12a2926433a23e5d4d35929a79d04486e2 Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 15 Apr 2026 19:22:39 +0300 Subject: [PATCH 13/59] Simplify escrow logic in `Nostromo`: unify per-asset and lot-based pricing in auctions, remove redundant bidder and escrow fields, refine bid processing procedures, and enhance refund and validation logic. --- src/contracts/Nostromo.h | 119 +++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 67 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index bc07d4189..03f26927d 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -89,7 +89,7 @@ struct NOST : public ContractBase /** @brief Quantity finally allocated to the participant after batch auction settlement. */ uint64 allocatedQuantity; - /** @brief Total amount committed by the participant for the current bid. */ + /** @brief Offered price per asset in a batch auction, or total offered price for the whole lot in a standard auction. */ uint64 bidAmount; /** @brief Timestamp of the participant's latest accepted bid. */ @@ -98,9 +98,6 @@ struct NOST : public ContractBase /** @brief Wallet that owns this participant record. */ id participant; - /** @brief Marks the participant that currently holds the leading bid. */ - uint8 isHighestBidder; - /** @brief Marks bids that remain inside the winning allocation after settlement. */ uint8 isWinningBid; }; @@ -142,7 +139,7 @@ struct NOST : public ContractBase /** @brief Initial price for a standard auction; bids cannot start below this total price for the whole lot. */ uint64 initialPrice; - /** @brief Sale price defined by the seller as the desired or minimum acceptable total selling price. */ + /** @brief Minimum acceptable 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. */ @@ -151,7 +148,7 @@ struct NOST : public ContractBase /** @brief Buy Now price that closes a standard auction immediately when matched or exceeded. */ uint64 buyNowPrice; - /** @brief Highest total bid currently offered by any active bid. */ + /** @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. */ @@ -175,9 +172,6 @@ struct NOST : public ContractBase /** @brief Timestamp when the auction was finalized, cancelled, or otherwise settled. */ DateAndTime settledAt; - /** @brief Number of distinct bidders that have placed bids in this auction. */ - uint32 bidderCount; - /** @brief Wallet that created the auction and offers the lot for sale. */ id seller; @@ -242,7 +236,7 @@ struct NOST : public ContractBase /** @brief Initial price for a standard auction; bids cannot be placed below this total price for the whole lot. */ uint64 initialPrice; - /** @brief Sale price defined by the seller as the desired or minimum acceptable total selling price. */ + /** @brief Minimum acceptable 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. */ @@ -280,7 +274,7 @@ struct NOST : public ContractBase /** @brief Requested quantity for a batch auction; ignored for a standard auction because the whole lot is sold as one unit. */ uint64 quantity; - /** @brief Total amount the bidder commits for this bid. */ + /** @brief Offered price per asset in a batch auction, or total offered price for the whole lot in a standard auction. */ uint64 bidAmount; }; @@ -449,12 +443,9 @@ struct NOST : public ContractBase /** @brief Quantity requested by the bidder in the batch auction. */ uint64 effectiveQuantity; - /** @brief Total amount the bidder commits for the batch bid. */ + /** @brief Offered price per asset for the requested quantity in the batch auction. */ uint64 bidAmount; - /** @brief Amount that must remain escrowed for the batch bid. */ - uint64 requiredEscrow; - /** @brief Timestamp of the accepted bid. */ DateAndTime currentDate; @@ -465,6 +456,9 @@ struct NOST : public ContractBase /** @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; @@ -481,6 +475,7 @@ struct NOST : public ContractBase AuctionParticipantData participantData; AuctionParticipantKey participantKey; uint64 previousEscrow; + uint64 requiredEscrow; uint8 participantExists; }; @@ -493,9 +488,6 @@ struct NOST : public ContractBase /** @brief Total amount the bidder commits for the standard auction lot. */ uint64 bidAmount; - /** @brief Amount that must remain escrowed for the standard bid. */ - uint64 requiredEscrow; - /** @brief Timestamp of the accepted bid. */ DateAndTime currentDate; @@ -506,6 +498,9 @@ struct NOST : public ContractBase /** @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; @@ -524,6 +519,7 @@ struct NOST : public ContractBase AuctionParticipantKey participantKey; AuctionParticipantKey highestBidderKey; uint64 previousEscrow; + uint64 requiredEscrow; uint8 participantExists; uint8 highestBidderExists; }; @@ -642,7 +638,6 @@ struct NOST : public ContractBase ProcessStandardBid_input processStandardBidInput; ProcessStandardBid_output processStandardBidOutput; uint64 elapsedSeconds; - uint64 requiredEscrow; DateAndTime currentDate; uint8 hasAccess; }; @@ -802,6 +797,7 @@ struct NOST : public ContractBase PRIVATE_PROCEDURE_WITH_LOCALS(ProcessBatchBid) { + output.escrowedAmount = 0; output.refundedAmount = 0; output.errorCode = static_cast(EAuctionError::Success); output.success = 0; @@ -823,31 +819,31 @@ struct NOST : public ContractBase return; } + locals.requiredEscrow = smul(input.effectiveQuantity, input.bidAmount); + if (static_cast(qpi.invocationReward()) < locals.requiredEscrow) + { + output.errorCode = static_cast(EAuctionError::InsufficientFunds); + return; + } + locals.participantKey = {input.auctionId, qpi.invocator()}; locals.participantExists = state.get().participants.get(locals.participantKey, locals.participantData); locals.previousEscrow = locals.participantExists ? locals.participantData.escrowedAmount : 0; - locals.participantData.escrowedAmount = input.requiredEscrow; + locals.participantData.escrowedAmount = locals.requiredEscrow; 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.isHighestBidder = 0; locals.participantData.isWinningBid = 0; - if (!locals.participantExists) - { - locals.auction.bidderCount = sadd(locals.auction.bidderCount, 1U); - } - if (input.bidAmount > locals.auction.highestBidPrice) { locals.auction.highestBidder = qpi.invocator(); locals.auction.highestBidPrice = input.bidAmount; locals.auction.highestBidQuantity = input.effectiveQuantity; - locals.auction.highestBidAmount = input.requiredEscrow; - locals.participantData.isHighestBidder = 1; + locals.auction.highestBidAmount = locals.requiredEscrow; } locals.auction.lastBidAt = input.currentDate; @@ -855,13 +851,6 @@ struct NOST : public ContractBase { locals.auction.auctionDurationSeconds = sadd(locals.auction.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); } - if (locals.auction.buyNowPrice > 0 && input.bidAmount >= locals.auction.buyNowPrice) - { - locals.auction.status = EAuctionStatus::Finalized; - locals.auction.settledAt = input.currentDate; - locals.participantData.isWinningBid = 1; - locals.participantData.isHighestBidder = 1; - } if (state.mut().participants.set(locals.participantKey, locals.participantData) == NULL_INDEX) { @@ -875,12 +864,19 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), locals.previousEscrow); output.refundedAmount = sadd(output.refundedAmount, locals.previousEscrow); } + if (static_cast(qpi.invocationReward()) > locals.requiredEscrow) + { + qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - locals.requiredEscrow); + output.refundedAmount = sadd(output.refundedAmount, static_cast(qpi.invocationReward()) - locals.requiredEscrow); + } + output.escrowedAmount = locals.requiredEscrow; output.success = 1; } PRIVATE_PROCEDURE_WITH_LOCALS(ProcessStandardBid) { + output.escrowedAmount = 0; output.refundedAmount = 0; output.errorCode = static_cast(EAuctionError::Success); output.success = 0; @@ -897,6 +893,13 @@ struct NOST : public ContractBase return; } + locals.requiredEscrow = input.bidAmount; + if (static_cast(qpi.invocationReward()) < locals.requiredEscrow) + { + output.errorCode = static_cast(EAuctionError::InsufficientFunds); + return; + } + if (locals.auction.highestBidPrice == 0) { if (input.bidAmount < locals.auction.initialPrice) @@ -915,20 +918,14 @@ struct NOST : public ContractBase locals.participantExists = state.get().participants.get(locals.participantKey, locals.participantData); locals.previousEscrow = locals.participantExists ? locals.participantData.escrowedAmount : 0; - locals.participantData.escrowedAmount = input.requiredEscrow; + locals.participantData.escrowedAmount = locals.requiredEscrow; locals.participantData.requestedQuantity = locals.auction.quantityForSale; locals.participantData.allocatedQuantity = 0; locals.participantData.bidAmount = input.bidAmount; locals.participantData.lastBidTime = input.currentDate; locals.participantData.participant = qpi.invocator(); - locals.participantData.isHighestBidder = 0; locals.participantData.isWinningBid = 0; - if (!locals.participantExists) - { - locals.auction.bidderCount = sadd(locals.auction.bidderCount, 1U); - } - if (!isZero(locals.auction.highestBidder)) { locals.highestBidderKey = {locals.auction.auctionId, locals.auction.highestBidder}; @@ -939,17 +936,15 @@ struct NOST : public ContractBase qpi.transfer(locals.previousHighestBidderData.participant, locals.previousHighestBidderData.escrowedAmount); output.refundedAmount = sadd(output.refundedAmount, locals.previousHighestBidderData.escrowedAmount); locals.previousHighestBidderData.escrowedAmount = 0; - locals.previousHighestBidderData.isHighestBidder = 0; locals.previousHighestBidderData.isWinningBid = 0; state.mut().participants.replace(locals.highestBidderKey, locals.previousHighestBidderData); } - locals.participantData.isHighestBidder = 1; locals.participantData.isWinningBid = 1; locals.auction.highestBidder = qpi.invocator(); locals.auction.highestBidPrice = input.bidAmount; locals.auction.highestBidQuantity = locals.auction.quantityForSale; - locals.auction.highestBidAmount = input.requiredEscrow; + locals.auction.highestBidAmount = locals.requiredEscrow; locals.auction.lastBidAt = input.currentDate; if ((locals.auction.auctionDurationSeconds - input.elapsedSeconds) <= NOST_AUCTION_EXTENSION_SECONDS) @@ -961,7 +956,6 @@ struct NOST : public ContractBase locals.auction.status = EAuctionStatus::Finalized; locals.auction.settledAt = input.currentDate; locals.participantData.isWinningBid = 1; - locals.participantData.isHighestBidder = 1; } if (state.mut().participants.set(locals.participantKey, locals.participantData) == NULL_INDEX) @@ -976,7 +970,13 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), locals.previousEscrow); output.refundedAmount = sadd(output.refundedAmount, locals.previousEscrow); } + if (static_cast(qpi.invocationReward()) > locals.requiredEscrow) + { + qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - locals.requiredEscrow); + output.refundedAmount = sadd(output.refundedAmount, static_cast(qpi.invocationReward()) - locals.requiredEscrow); + } + output.escrowedAmount = locals.requiredEscrow; output.success = 1; } @@ -1360,24 +1360,12 @@ struct NOST : public ContractBase } } - locals.requiredEscrow = input.bidAmount; - if (static_cast(qpi.invocationReward()) < locals.requiredEscrow) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = static_cast(EAuctionError::InsufficientFunds); - return; - } - switch (locals.auction.type) { case EAuctionType::Batch: locals.processBatchBidInput.auctionId = input.auctionId; locals.processBatchBidInput.effectiveQuantity = input.quantity; locals.processBatchBidInput.bidAmount = input.bidAmount; - locals.processBatchBidInput.requiredEscrow = locals.requiredEscrow; locals.processBatchBidInput.currentDate = locals.currentDate; locals.processBatchBidInput.elapsedSeconds = locals.elapsedSeconds; CALL(ProcessBatchBid, locals.processBatchBidInput, locals.processBatchBidOutput); @@ -1391,11 +1379,11 @@ struct NOST : public ContractBase return; } output.refundedAmount = sadd(output.refundedAmount, locals.processBatchBidOutput.refundedAmount); + output.escrowedAmount = locals.processBatchBidOutput.escrowedAmount; break; case EAuctionType::Standard: locals.processStandardBidInput.auctionId = input.auctionId; locals.processStandardBidInput.bidAmount = input.bidAmount; - locals.processStandardBidInput.requiredEscrow = locals.requiredEscrow; locals.processStandardBidInput.currentDate = locals.currentDate; locals.processStandardBidInput.elapsedSeconds = locals.elapsedSeconds; CALL(ProcessStandardBid, locals.processStandardBidInput, locals.processStandardBidOutput); @@ -1409,6 +1397,7 @@ struct NOST : public ContractBase return; } output.refundedAmount = sadd(output.refundedAmount, locals.processStandardBidOutput.refundedAmount); + output.escrowedAmount = locals.processStandardBidOutput.escrowedAmount; break; default: if (qpi.invocationReward() > 0) @@ -1418,14 +1407,6 @@ struct NOST : public ContractBase output.errorCode = static_cast(EAuctionError::InvalidAuctionType); return; } - - if (static_cast(qpi.invocationReward()) > locals.requiredEscrow) - { - qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - locals.requiredEscrow); - output.refundedAmount = sadd(output.refundedAmount, static_cast(qpi.invocationReward()) - locals.requiredEscrow); - } - - output.escrowedAmount = locals.requiredEscrow; output.errorCode = static_cast(EAuctionError::Success); } @@ -1464,6 +1445,10 @@ struct NOST : public ContractBase } locals.cancellationBaseAmount = max(locals.auction.highestBidAmount, locals.auction.salePrice); + if (locals.auction.type == EAuctionType::Batch) + { + locals.cancellationBaseAmount = max(locals.auction.highestBidAmount, smul(locals.auction.salePrice, locals.auction.quantityForSale)); + } output.cancellationFee = div(smul(locals.cancellationBaseAmount, state.get().auctionCancellationFeeBasisPoints), 10000ULL); if (static_cast(qpi.invocationReward()) < output.cancellationFee) From d76de93e467d45e1662db28ac646346f313e3772 Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 15 Apr 2026 19:47:40 +0300 Subject: [PATCH 14/59] Update comments in `Nostromo` and add participant cleanup in auction rollback: refine `@brief` documentation for price fields, standardize comment formatting, and invoke `participants.cleanupIfNeeded()` during rollback. --- src/contracts/Nostromo.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 03f26927d..b07ef81d5 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -139,7 +139,8 @@ struct NOST : public ContractBase /** @brief Initial price for a standard auction; bids cannot start below this total price for the whole lot. */ uint64 initialPrice; - /** @brief Minimum acceptable price per asset in a batch auction, or desired minimum total selling price for the whole lot in a standard auction. */ + /** @brief Minimum acceptable 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. */ @@ -236,7 +237,8 @@ struct NOST : public ContractBase /** @brief Initial price for a standard auction; bids cannot be placed below this total price for the whole lot. */ uint64 initialPrice; - /** @brief Minimum acceptable price per asset in a batch auction, or desired minimum total selling price for the whole lot in a standard auction. */ + /** @brief Minimum acceptable 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. */ @@ -1478,6 +1480,7 @@ struct NOST : public ContractBase locals.participantIndex = state.get().participants.nextElementIndex(locals.participantIndex); } + state.mut().participants.cleanupIfNeeded(); locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.auctionLotItems; CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); From e65941dd1ffcf5776853891769ff0a9e6e3bb2ea Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 15 Apr 2026 20:00:09 +0300 Subject: [PATCH 15/59] Add storage capacity check in `Nostromo` auction participation: prevent new participants if storage is full and return `StorageFull` error code. --- src/contracts/Nostromo.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index b07ef81d5..60b3ff166 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -919,6 +919,11 @@ struct NOST : public ContractBase locals.participantKey = {input.auctionId, qpi.invocator()}; locals.participantExists = state.get().participants.get(locals.participantKey, locals.participantData); locals.previousEscrow = locals.participantExists ? locals.participantData.escrowedAmount : 0; + if (!locals.participantExists && state.get().participants.population() >= state.get().participants.capacity()) + { + output.errorCode = static_cast(EAuctionError::StorageFull); + return; + } locals.participantData.escrowedAmount = locals.requiredEscrow; locals.participantData.requestedQuantity = locals.auction.quantityForSale; From 53173b51ed7eb0f48116cfed13c8ee2887d8e7c1 Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 15 Apr 2026 21:11:09 +0300 Subject: [PATCH 16/59] Add auction pause handling and tick-based scheduling in `Nostromo`: introduce post-epoch auction pause mechanism, tick-based scheduling for auction finalization, and new procedures for batch and standard auction finalization. --- src/contract_core/qpi_system_impl.h | 5 + src/contracts/Nostromo.h | 455 +++++++++++++++++++++++++++- src/contracts/qpi.h | 2 + 3 files changed, 455 insertions(+), 7 deletions(-) diff --git a/src/contract_core/qpi_system_impl.h b/src/contract_core/qpi_system_impl.h index dc7f19473..d0acd87ed 100644 --- a/src/contract_core/qpi_system_impl.h +++ b/src/contract_core/qpi_system_impl.h @@ -12,3 +12,8 @@ unsigned int QPI::QpiContextFunctionCall::tick() const { return system.tick; } + +unsigned int QPI::QpiContextFunctionCall::initialTick() const +{ + return system.initialTick; +} diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 60b3ff166..140c862e7 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -20,6 +20,8 @@ constexpr uint64 NOST_AUCTION_CANCELLATION_FEE_BP = 1000ULL; constexpr uint64 NOST_AUCTION_EXTENSION_SECONDS = 300ULL; constexpr uint64 NOST_SECONDS_PER_DAY = 86400ULL; constexpr uint64 NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS = 604800ULL; +constexpr uint64 NOST_AUCTION_PRE_EPOCH_PAUSE_SECONDS = 1800ULL; +constexpr uint32 NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS = 500U; struct NOST2 { @@ -63,7 +65,8 @@ struct NOST : public ContractBase InvalidAuctionType, InvalidVisibility, BidTooLow, - PrivateAuctionAccessDenied + PrivateAuctionAccessDenied, + AuctionPaused }; struct AuctionParticipantKey @@ -212,6 +215,9 @@ struct NOST : public ContractBase /** @brief Configured maximum auction duration in days. */ uint32 maxAuctionDurationDays; + /** @brief Flag indicating whether the post-`BEGIN_EPOCH()` auction pause is active for the current epoch. */ + uint8 isPostBeginEpochPauseArmed; + HashMap auctionList; HashMap participants; }; @@ -347,6 +353,15 @@ struct NOST : public ContractBase uint8 found; }; + struct GetTicksBeforeAuctionLaunch_input + { + }; + + struct GetTicksBeforeAuctionLaunch_output + { + uint32 ticks; + }; + /** @brief Internal input used to validate an auction lot and resolve its total escrow quantity. */ struct AnalyzeAuctionLot_input { @@ -436,6 +451,46 @@ struct NOST : public ContractBase sint64 possessedAccessShares; }; + struct FinalizeBatchAuction_input + { + DateAndTime currentDate; + id auctionId; + }; + + struct FinalizeBatchAuction_output + { + uint8 success; + }; + + struct FinalizeStandardAuction_input + { + DateAndTime currentDate; + id auctionId; + }; + + struct FinalizeStandardAuction_output + { + uint8 success; + }; + + struct IsAuctionInteractionPaused_input + { + }; + + struct IsAuctionInteractionPaused_output + { + uint8 isPaused; + }; + + struct GetTicksBeforeAuctionLaunchInternal_input + { + }; + + struct GetTicksBeforeAuctionLaunchInternal_output + { + uint32 ticks; + }; + /** @brief Internal input used to process a batch auction bid after the common PlaceBid checks succeed. */ struct ProcessBatchBid_input { @@ -520,10 +575,13 @@ struct NOST : public ContractBase AuctionParticipantData previousHighestBidderData; AuctionParticipantKey participantKey; AuctionParticipantKey highestBidderKey; + FinalizeStandardAuction_input finalizeStandardAuctionInput; + FinalizeStandardAuction_output finalizeStandardAuctionOutput; uint64 previousEscrow; uint64 requiredEscrow; uint8 participantExists; uint8 highestBidderExists; + uint8 finalizeImmediately; }; /** @brief Internal input used to validate the IPFS metadata CID format required by the Auction House. */ @@ -591,11 +649,14 @@ struct NOST : public ContractBase sint64 transferredShares; }; - /** @brief Internal input used to roll back an auction lot escrow attempt or to return the lot after cancellation. */ + /** @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 returned from contract escrow to the seller. */ + /** @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; }; typedef NoData RollbackAuctionLotAssets_output; @@ -606,9 +667,42 @@ struct NOST : public ContractBase uint64 lotItemIndex; }; + struct FinalizeBatchAuction_locals + { + AuctionData auction; + AuctionParticipantData participantData; + AuctionParticipantData bestParticipantData; + AuctionParticipantKey participantKey; + AuctionParticipantKey bestParticipantKey; + AuctionLotEntry batchLotItem; + DateAndTime currentDate; + uint64 remainingQuantity; + uint64 allocatedQuantity; + uint64 requiredPayment; + uint64 refundAmount; + uint64 soldQuantity; + uint64 lotItemIndex; + sint64 participantIndex; + uint8 bestParticipantFound; + uint8 lotItemFound; + }; + + struct FinalizeStandardAuction_locals + { + AuctionData auction; + AuctionParticipantData highestBidderData; + AuctionParticipantKey highestBidderKey; + RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; + RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; + uint8 highestBidderExists; + uint8 lotSold; + }; + struct CreateAuction_locals { AuctionData auction; + IsAuctionInteractionPaused_input isAuctionInteractionPausedInput; + IsAuctionInteractionPaused_output isAuctionInteractionPausedOutput; ValidateMetadataCid_input validateMetadataCidInput; ValidateMetadataCid_output validateMetadataCidOutput; AnalyzeAuctionLot_input analyzeAuctionLotInput; @@ -633,6 +727,8 @@ struct NOST : public ContractBase struct PlaceBid_locals { AuctionData auction; + IsAuctionInteractionPaused_input isAuctionInteractionPausedInput; + IsAuctionInteractionPaused_output isAuctionInteractionPausedOutput; HasRequiredAccessAsset_input hasRequiredAccessAssetInput; HasRequiredAccessAsset_output hasRequiredAccessAssetOutput; ProcessBatchBid_input processBatchBidInput; @@ -656,6 +752,20 @@ struct NOST : public ContractBase sint64 participantIndex; }; + struct END_TICK_locals + { + AuctionData auction; + DateAndTime currentDate; + uint64 elapsedSeconds; + sint64 auctionIndex; + GetTicksBeforeAuctionLaunchInternal_input getTicksBeforeAuctionLaunchInternalInput; + GetTicksBeforeAuctionLaunchInternal_output getTicksBeforeAuctionLaunchInternalOutput; + FinalizeBatchAuction_input finalizeBatchAuctionInput; + FinalizeBatchAuction_output finalizeBatchAuctionOutput; + FinalizeStandardAuction_input finalizeStandardAuctionInput; + FinalizeStandardAuction_output finalizeStandardAuctionOutput; + }; + /** @brief Input payload used to move share management rights to another managing contract. */ struct TransferShareManagementRights_input { @@ -685,6 +795,7 @@ struct NOST : public ContractBase REGISTER_USER_FUNCTION(GetAuction, 1); REGISTER_USER_FUNCTION(GetAuctionParticipant, 2); + REGISTER_USER_FUNCTION(GetTicksBeforeAuctionLaunch, 3); } INITIALIZE() @@ -710,6 +821,8 @@ struct NOST : public ContractBase state.mut().auctionCancellationFeeBasisPoints = NOST_AUCTION_CANCELLATION_FEE_BP; state.mut().maxAuctionDurationDays = NOST_AUCTION_MAX_DURATION_DAYS; } + + state.mut().isPostBeginEpochPauseArmed = 1; } END_EPOCH() @@ -718,6 +831,67 @@ struct NOST : public ContractBase state.mut().participants.cleanupIfNeeded(); } + END_TICK_WITH_LOCALS() + { + if (state.get().isPostBeginEpochPauseArmed) + { + CALL(GetTicksBeforeAuctionLaunchInternal, locals.getTicksBeforeAuctionLaunchInternalInput, + locals.getTicksBeforeAuctionLaunchInternalOutput); + if (locals.getTicksBeforeAuctionLaunchInternalOutput.ticks == 0) + { + state.mut().isPostBeginEpochPauseArmed = 0; + } + else + { + return; + } + } + + locals.currentDate = qpi.now(); + locals.auctionIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); + while (locals.auctionIndex != NULL_INDEX) + { + locals.auction = state.get().auctionList.value(locals.auctionIndex); + if (locals.auction.status == EAuctionStatus::Active) + { + diffDateInSecond(locals.auction.createdAt, locals.currentDate, locals.elapsedSeconds); + if (locals.elapsedSeconds >= locals.auction.auctionDurationSeconds) + { + if (locals.auction.type == EAuctionType::Batch) + { + locals.finalizeBatchAuctionInput.auctionId = locals.auction.auctionId; + locals.finalizeBatchAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeBatchAuction, locals.finalizeBatchAuctionInput, locals.finalizeBatchAuctionOutput); + } + else if (locals.auction.type == EAuctionType::Standard) + { + if (locals.auction.highestBidAmount == 0 || locals.auction.highestBidPrice >= locals.auction.salePrice) + { + locals.finalizeStandardAuctionInput.auctionId = locals.auction.auctionId; + locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); + } + else + { + locals.auction.status = EAuctionStatus::PendingSellerDecision; + locals.auction.sellerDecisionDeadline = locals.currentDate; + locals.auction.sellerDecisionDeadline.add(0, 0, 0, 0, 0, NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS); + state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); + } + } + } + } + else if (locals.auction.status == EAuctionStatus::PendingSellerDecision && locals.auction.sellerDecisionDeadline <= locals.currentDate) + { + locals.finalizeStandardAuctionInput.auctionId = locals.auction.auctionId; + locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); + } + + locals.auctionIndex = state.get().auctionList.nextElementIndex(locals.auctionIndex); + } + } + PRIVATE_FUNCTION_WITH_LOCALS(AnalyzeAuctionLot) { output.totalEscrowQuantity = 0; @@ -753,6 +927,50 @@ struct NOST : public ContractBase output.isValid = output.lotItemCount > 0 ? 1 : 0; } + PRIVATE_FUNCTION(IsAuctionInteractionPaused) + { + output.isPaused = 0; + + if (state.get().isPostBeginEpochPauseArmed && max(static_cast(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) - + (static_cast(qpi.tick()) - static_cast(qpi.initialTick())), + 0) > 0) + { + output.isPaused = 1; + return; + } + + if (qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()) != 0) + { + return; + } + + if (qpi.hour() < 11 || qpi.hour() > 11) + { + return; + } + + if (qpi.minute() < 30) + { + return; + } + + output.isPaused = 1; + } + + PRIVATE_FUNCTION(GetTicksBeforeAuctionLaunchInternal) + { + output.ticks = 0; + + 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)); + } + PRIVATE_FUNCTION_WITH_LOCALS(CountAllowedBidderWallets) { output.allowedWalletCount = 0; @@ -883,6 +1101,7 @@ struct NOST : public ContractBase output.errorCode = static_cast(EAuctionError::Success); output.success = 0; locals.highestBidderExists = 0; + locals.finalizeImmediately = 0; if (!state.get().auctionList.get(input.auctionId, locals.auction)) { output.errorCode = static_cast(EAuctionError::AuctionNotFound); @@ -960,9 +1179,7 @@ struct NOST : public ContractBase } if (locals.auction.buyNowPrice > 0 && input.bidAmount >= locals.auction.buyNowPrice) { - locals.auction.status = EAuctionStatus::Finalized; - locals.auction.settledAt = input.currentDate; - locals.participantData.isWinningBid = 1; + locals.finalizeImmediately = 1; } if (state.mut().participants.set(locals.participantKey, locals.participantData) == NULL_INDEX) @@ -985,6 +1202,13 @@ struct NOST : public ContractBase output.escrowedAmount = locals.requiredEscrow; output.success = 1; + + if (locals.finalizeImmediately) + { + locals.finalizeStandardAuctionInput.auctionId = input.auctionId; + locals.finalizeStandardAuctionInput.currentDate = input.currentDate; + CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); + } } PRIVATE_FUNCTION_WITH_LOCALS(ValidateMetadataCid) @@ -1060,10 +1284,189 @@ struct NOST : public ContractBase continue; } qpi.transferShareOwnershipAndPossession(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, SELF, SELF, locals.lotItem.quantity, - qpi.invocator()); + input.recipient); } } + PRIVATE_PROCEDURE_WITH_LOCALS(FinalizeBatchAuction) + { + output.success = 0; + locals.bestParticipantFound = 0; + locals.lotItemFound = 0; + locals.soldQuantity = 0; + + if (!state.get().auctionList.get(input.auctionId, locals.auction)) + { + return; + } + + if (locals.auction.type != EAuctionType::Batch || locals.auction.status != EAuctionStatus::Active) + { + return; + } + + for (locals.lotItemIndex = 0; locals.lotItemIndex < NOST_AUCTION_LOT_ITEM_NUM; ++locals.lotItemIndex) + { + locals.batchLotItem = locals.auction.auctionLotItems.get(locals.lotItemIndex); + if (!isZeroAsset(locals.batchLotItem.asset) && locals.batchLotItem.quantity > 0) + { + locals.lotItemFound = 1; + break; + } + } + if (!locals.lotItemFound) + { + return; + } + + locals.remainingQuantity = locals.auction.quantityForSale; + while (locals.remainingQuantity > 0) + { + locals.bestParticipantFound = 0; + locals.participantIndex = state.get().participants.nextElementIndex(NULL_INDEX); + while (locals.participantIndex != NULL_INDEX) + { + locals.participantKey = state.get().participants.key(locals.participantIndex); + if (locals.participantKey.auctionId == input.auctionId) + { + locals.participantData = state.get().participants.value(locals.participantIndex); + if (locals.participantData.escrowedAmount > 0) + { + if (!locals.bestParticipantFound || locals.participantData.bidAmount > locals.bestParticipantData.bidAmount || + (locals.participantData.bidAmount == locals.bestParticipantData.bidAmount && + locals.participantData.lastBidTime < locals.bestParticipantData.lastBidTime)) + { + locals.bestParticipantFound = 1; + locals.bestParticipantData = locals.participantData; + locals.bestParticipantKey = locals.participantKey; + } + } + } + locals.participantIndex = state.get().participants.nextElementIndex(locals.participantIndex); + } + + if (!locals.bestParticipantFound) + { + break; + } + + 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; + } + + if (locals.allocatedQuantity > 0) + { + qpi.transfer(locals.auction.seller, locals.requiredPayment); + 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.remainingQuantity -= locals.allocatedQuantity; + } + + if (locals.refundAmount > 0) + { + qpi.transfer(locals.bestParticipantData.participant, locals.refundAmount); + } + + locals.bestParticipantData.escrowedAmount = 0; + state.mut().participants.replace(locals.bestParticipantKey, locals.bestParticipantData); + } + + locals.participantIndex = state.get().participants.nextElementIndex(NULL_INDEX); + while (locals.participantIndex != NULL_INDEX) + { + locals.participantKey = state.get().participants.key(locals.participantIndex); + if (locals.participantKey.auctionId == input.auctionId) + { + locals.participantData = state.get().participants.value(locals.participantIndex); + if (locals.participantData.escrowedAmount > 0) + { + qpi.transfer(locals.participantData.participant, locals.participantData.escrowedAmount); + locals.participantData.escrowedAmount = 0; + locals.participantData.allocatedQuantity = 0; + locals.participantData.isWinningBid = 0; + state.mut().participants.replace(locals.participantKey, locals.participantData); + } + } + locals.participantIndex = state.get().participants.nextElementIndex(locals.participantIndex); + } + + if (locals.soldQuantity < locals.auction.quantityForSale) + { + qpi.transferShareOwnershipAndPossession(locals.batchLotItem.asset.assetName, locals.batchLotItem.asset.issuer, SELF, SELF, + locals.auction.quantityForSale - locals.soldQuantity, locals.auction.seller); + } + + locals.auction.allocatedQuantity = locals.soldQuantity; + locals.auction.status = EAuctionStatus::Finalized; + locals.auction.settledAt = input.currentDate; + state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); + output.success = 1; + } + + PRIVATE_PROCEDURE_WITH_LOCALS(FinalizeStandardAuction) + { + output.success = 0; + locals.highestBidderExists = 0; + locals.lotSold = 0; + + if (!state.get().auctionList.get(input.auctionId, locals.auction)) + { + return; + } + + if (locals.auction.type != EAuctionType::Standard) + { + return; + } + + if (!isZero(locals.auction.highestBidder)) + { + locals.highestBidderKey = {locals.auction.auctionId, locals.auction.highestBidder}; + locals.highestBidderExists = state.get().participants.get(locals.highestBidderKey, locals.highestBidderData); + } + + if (locals.highestBidderExists && locals.highestBidderData.escrowedAmount > 0) + { + qpi.transfer(locals.auction.seller, locals.highestBidderData.escrowedAmount); + locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = locals.highestBidderData.participant; + CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); + + locals.highestBidderData.allocatedQuantity = locals.auction.quantityForSale; + locals.highestBidderData.isWinningBid = 1; + locals.highestBidderData.escrowedAmount = 0; + state.mut().participants.replace(locals.highestBidderKey, locals.highestBidderData); + locals.auction.allocatedQuantity = locals.auction.quantityForSale; + locals.lotSold = 1; + } + else + { + locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.seller; + CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); + locals.auction.allocatedQuantity = 0; + } + + locals.auction.status = EAuctionStatus::Finalized; + locals.auction.settledAt = input.currentDate; + if (!locals.lotSold) + { + locals.auction.highestBidAmount = 0; + locals.auction.highestBidPrice = 0; + locals.auction.highestBidQuantity = 0; + locals.auction.highestBidder = NULL_ID; + } + state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); + output.success = 1; + } + PRIVATE_PROCEDURE_WITH_LOCALS(EscrowAuctionLotAssets) { output.success = 1; @@ -1104,6 +1507,17 @@ struct NOST : public ContractBase { output.errorCode = static_cast(EAuctionError::InvalidInput); + CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); + if (locals.isAuctionInteractionPausedOutput.isPaused) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::AuctionPaused); + return; + } + if (!isSupportedAuctionType(static_cast(input.auctionType))) { if (qpi.invocationReward() > 0) @@ -1279,6 +1693,7 @@ struct NOST : public ContractBase if (state.mut().auctionList.set(locals.auction.auctionId, locals.auction) == NULL_INDEX) { locals.rollbackAuctionLotAssetsInput.auctionLotItems = input.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = qpi.invocator(); CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); if (qpi.invocationReward() > 0) { @@ -1301,6 +1716,17 @@ struct NOST : public ContractBase { output.errorCode = static_cast(EAuctionError::InvalidInput); + CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); + if (locals.isAuctionInteractionPausedOutput.isPaused) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::AuctionPaused); + return; + } + if (!state.get().auctionList.get(input.auctionId, locals.auction)) { if (qpi.invocationReward() > 0) @@ -1488,6 +1914,7 @@ struct NOST : public ContractBase state.mut().participants.cleanupIfNeeded(); locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.seller; CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); locals.currentDate = qpi.now(); @@ -1510,6 +1937,20 @@ struct NOST : public ContractBase output.found = state.get().participants.get({input.auctionId, input.participant}, output.participantData) ? 1 : 0; } + PUBLIC_FUNCTION(GetTicksBeforeAuctionLaunch) + { + output.ticks = 0; + + 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)); + } + PUBLIC_PROCEDURE(TransferShareManagementRights) { if (qpi.invocationReward() > 0) diff --git a/src/contracts/qpi.h b/src/contracts/qpi.h index 63798b43c..30f253bcb 100644 --- a/src/contracts/qpi.h +++ b/src/contracts/qpi.h @@ -2481,6 +2481,8 @@ 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) From 41043f1763c023dec826af5230a2d6982d8a8d3b Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 15 Apr 2026 21:22:03 +0300 Subject: [PATCH 17/59] Refine `minimumPurchaseQuantity` handling in `Nostromo`: update comments for clarity, remove unused checks in input validation, and simplify auction setup logic. --- src/contracts/Nostromo.h | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 140c862e7..267fa952c 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -136,13 +136,13 @@ struct NOST : public ContractBase /** @brief Quantity already assigned to winning bids after settlement. */ uint64 allocatedQuantity; - /** @brief Minimum quantity a bidder may request in a batch auction; standard auctions always sell the whole lot as one unit. */ + /** @brief Reserved for standard auction validation; batch auctions do not enforce a minimum bid quantity. */ uint64 minimumPurchaseQuantity; /** @brief Initial price for a standard auction; bids cannot start below this total price for the whole lot. */ uint64 initialPrice; - /** @brief Minimum acceptable price per asset in a batch auction, or desired minimum total selling price for the whole lot in a standard + /** @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; @@ -237,13 +237,13 @@ struct NOST : public ContractBase /** @brief Wallet list used when the private auction restricts participation to predefined wallets. */ Array allowedBidderWallets; - /** @brief Minimum quantity a bidder may request in a batch auction; standard auctions always sell the whole lot as one unit. */ + /** @brief Reserved for standard auction validation; batch auctions ignore this value. */ 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 acceptable price per asset in a batch auction, or desired minimum total selling price for the whole lot in a standard + /** @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; @@ -1027,7 +1027,7 @@ struct NOST : public ContractBase return; } - if (input.effectiveQuantity == 0 || input.effectiveQuantity < locals.auction.minimumPurchaseQuantity || input.bidAmount == 0) + if (input.effectiveQuantity == 0 || input.bidAmount == 0) { output.errorCode = static_cast(EAuctionError::InvalidInput); return; @@ -1999,12 +1999,13 @@ struct NOST : public ContractBase { quantityForSale = 0; resolvedMinimumPurchaseQuantity = 0; - if (lotItemCount != 1 || minimumPurchaseQuantity == 0 || minimumPurchaseQuantity > totalEscrowQuantity || buyNowPrice != 0) + if (lotItemCount != 1 || totalEscrowQuantity == 0 || buyNowPrice != 0) { return false; } quantityForSale = totalEscrowQuantity; - resolvedMinimumPurchaseQuantity = minimumPurchaseQuantity; + (void)minimumPurchaseQuantity; + resolvedMinimumPurchaseQuantity = 0; return true; } From aa9ab4597ca539e2f28e9d8dda48f490fa296fa1 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 16 Apr 2026 19:01:24 +0300 Subject: [PATCH 18/59] Add revenue distribution and seller decision handling in `Nostromo`: implement `DistributeAuctionRevenue` for fee allocation, introduce `ResolvePendingStandardAuction` for seller decisions, and refine auction finalization procedures to support revenue sharing logic. --- src/contracts/Nostromo.h | 323 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 313 insertions(+), 10 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 267fa952c..94b7bf511 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -17,6 +17,17 @@ constexpr uint64 NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM = 16; constexpr uint32 NOST_AUCTION_MAX_DURATION_DAYS = 30; constexpr sint64 NOST_PRIVATE_AUCTION_FEE = 50000000LL; constexpr uint64 NOST_AUCTION_CANCELLATION_FEE_BP = 1000ULL; +constexpr uint64 NOST_AUCTION_MANAGEMENT_FEE_BP = 50ULL; +constexpr uint64 NOST_AUCTION_DEVELOPMENT_FEE_BP = 50ULL; +constexpr uint64 NOST_AUCTION_TAKEOVER_COORDINATOR_FEE_BP = 50ULL; +constexpr uint64 NOST_AUCTION_SHAREHOLDER_DIVIDEND_BP = 9000ULL; +constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_BP_TIER_1 = 500ULL; +constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_BP_TIER_2 = 450ULL; +constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_BP_TIER_3 = 400ULL; +constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_BP_TIER_4 = 350ULL; +constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1 = 5000000000ULL; +constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2 = 50000000000ULL; +constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3 = 200000000000ULL; constexpr uint64 NOST_AUCTION_EXTENSION_SECONDS = 300ULL; constexpr uint64 NOST_SECONDS_PER_DAY = 86400ULL; constexpr uint64 NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS = 604800ULL; @@ -212,12 +223,21 @@ struct NOST : public ContractBase /** @brief Configured cancellation fee rate in basis points. */ uint64 auctionCancellationFeeBasisPoints; + /** @brief Undistributed auction shareholder revenue reserved for contract dividends. */ + uint64 auctionShareholderDividendPool; + /** @brief Configured maximum auction duration in days. */ uint32 maxAuctionDurationDays; /** @brief Flag indicating whether the post-`BEGIN_EPOCH()` auction pause is active for the current epoch. */ uint8 isPostBeginEpochPauseArmed; + id management; + + id development; + + id takeoverCoordinator; + HashMap auctionList; HashMap participants; }; @@ -319,6 +339,26 @@ struct NOST : public ContractBase uint8 errorCode; }; + /** @brief Input payload used by the seller to accept or reject a pending standard auction result. */ + struct ResolvePendingStandardAuction_input + { + /** @brief Identifier of the standard auction awaiting the seller decision. */ + id auctionId; + + /** @brief Set to `1` to accept the sale or `0` to reject it. */ + uint8 acceptSale; + }; + + /** @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. */ + uint8 errorCode; + }; + /** @brief Input payload used to fetch one auction from storage. */ struct GetAuction_input { @@ -473,6 +513,18 @@ struct NOST : public ContractBase uint8 success; }; + struct RejectStandardAuction_input + { + DateAndTime currentDate; + id auctionId; + }; + + struct RejectStandardAuction_output + { + uint64 refundedAmount; + uint8 success; + }; + struct IsAuctionInteractionPaused_input { }; @@ -482,6 +534,17 @@ struct NOST : public ContractBase uint8 isPaused; }; + struct DistributeAuctionRevenue_input + { + uint64 grossAmount; + }; + + struct DistributeAuctionRevenue_output + { + uint64 sellerPayout; + uint8 success; + }; + struct GetTicksBeforeAuctionLaunchInternal_input { }; @@ -675,12 +738,15 @@ struct NOST : public ContractBase AuctionParticipantKey participantKey; AuctionParticipantKey bestParticipantKey; AuctionLotEntry batchLotItem; + DistributeAuctionRevenue_input distributeAuctionRevenueInput; + DistributeAuctionRevenue_output distributeAuctionRevenueOutput; DateAndTime currentDate; uint64 remainingQuantity; uint64 allocatedQuantity; uint64 requiredPayment; uint64 refundAmount; uint64 soldQuantity; + uint64 totalGrossAmount; uint64 lotItemIndex; sint64 participantIndex; uint8 bestParticipantFound; @@ -694,10 +760,34 @@ struct NOST : public ContractBase AuctionParticipantKey highestBidderKey; RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; + DistributeAuctionRevenue_input distributeAuctionRevenueInput; + DistributeAuctionRevenue_output distributeAuctionRevenueOutput; uint8 highestBidderExists; uint8 lotSold; }; + struct DistributeAuctionRevenue_locals + { + uint64 shareholderFeeBasisPoints; + uint64 shareholderFeeAmount; + uint64 managementFeeAmount; + uint64 developmentFeeAmount; + uint64 takeoverCoordinatorFeeAmount; + uint64 shareholderDividendAmount; + uint64 distributedDividendAmount; + uint64 dividendPerShare; + }; + + struct RejectStandardAuction_locals + { + AuctionData auction; + AuctionParticipantData highestBidderData; + AuctionParticipantKey highestBidderKey; + RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; + RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; + uint8 highestBidderExists; + }; + struct CreateAuction_locals { AuctionData auction; @@ -752,6 +842,16 @@ struct NOST : public ContractBase sint64 participantIndex; }; + struct ResolvePendingStandardAuction_locals + { + AuctionData auction; + DateAndTime currentDate; + FinalizeStandardAuction_input finalizeStandardAuctionInput; + FinalizeStandardAuction_output finalizeStandardAuctionOutput; + RejectStandardAuction_input rejectStandardAuctionInput; + RejectStandardAuction_output rejectStandardAuctionOutput; + }; + struct END_TICK_locals { AuctionData auction; @@ -792,6 +892,7 @@ struct NOST : public ContractBase REGISTER_USER_PROCEDURE(PlaceBid, 2); REGISTER_USER_PROCEDURE(CancelAuction, 3); REGISTER_USER_PROCEDURE(TransferShareManagementRights, 4); + REGISTER_USER_PROCEDURE(ResolvePendingStandardAuction, 5); REGISTER_USER_FUNCTION(GetAuction, 1); REGISTER_USER_FUNCTION(GetAuctionParticipant, 2); @@ -803,6 +904,13 @@ struct NOST : public ContractBase state.mut().privateAuctionFee = NOST_PRIVATE_AUCTION_FEE; state.mut().auctionCancellationFeeBasisPoints = NOST_AUCTION_CANCELLATION_FEE_BP; state.mut().maxAuctionDurationDays = NOST_AUCTION_MAX_DURATION_DAYS; + 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); } PRE_ACQUIRE_SHARES() @@ -820,6 +928,15 @@ struct NOST : public ContractBase state.mut().privateAuctionFee = NOST_PRIVATE_AUCTION_FEE; state.mut().auctionCancellationFeeBasisPoints = NOST_AUCTION_CANCELLATION_FEE_BP; state.mut().maxAuctionDurationDays = NOST_AUCTION_MAX_DURATION_DAYS; + 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); } state.mut().isPostBeginEpochPauseArmed = 1; @@ -903,7 +1020,7 @@ struct NOST : public ContractBase return; } - for (locals.lotItemIndex = 0; locals.lotItemIndex < NOST_AUCTION_LOT_ITEM_NUM; ++locals.lotItemIndex) + for (locals.lotItemIndex = 0; locals.lotItemIndex < input.auctionLotItems.capacity(); ++locals.lotItemIndex) { locals.lotItem = input.auctionLotItems.get(locals.lotItemIndex); if (isZeroAsset(locals.lotItem.asset)) @@ -971,10 +1088,55 @@ struct NOST : public ContractBase 0)); } + PRIVATE_PROCEDURE_WITH_LOCALS(DistributeAuctionRevenue) + { + output.sellerPayout = input.grossAmount; + output.success = 0; + + if (input.grossAmount == 0) + { + output.success = 1; + return; + } + + locals.shareholderFeeBasisPoints = getAuctionShareholderFeeBasisPoints(input.grossAmount); + locals.shareholderFeeAmount = div(smul(input.grossAmount, locals.shareholderFeeBasisPoints), 10000ULL); + locals.managementFeeAmount = div(smul(input.grossAmount, NOST_AUCTION_MANAGEMENT_FEE_BP), 10000ULL); + locals.developmentFeeAmount = div(smul(input.grossAmount, NOST_AUCTION_DEVELOPMENT_FEE_BP), 10000ULL); + locals.shareholderDividendAmount = div(smul(locals.shareholderFeeAmount, NOST_AUCTION_SHAREHOLDER_DIVIDEND_BP), 10000ULL); + locals.takeoverCoordinatorFeeAmount = div(smul(input.grossAmount, NOST_AUCTION_TAKEOVER_COORDINATOR_FEE_BP), 10000ULL) + + (locals.shareholderFeeAmount - locals.shareholderDividendAmount); + output.sellerPayout = input.grossAmount - locals.shareholderFeeAmount - locals.managementFeeAmount - locals.developmentFeeAmount - + div(smul(input.grossAmount, NOST_AUCTION_TAKEOVER_COORDINATOR_FEE_BP), 10000ULL); + + state.mut().auctionShareholderDividendPool = sadd(state.get().auctionShareholderDividendPool, locals.shareholderDividendAmount); + if (locals.managementFeeAmount > 0) + { + qpi.transfer(state.get().management, locals.managementFeeAmount); + } + if (locals.developmentFeeAmount > 0) + { + qpi.transfer(state.get().development, locals.developmentFeeAmount); + } + if (locals.takeoverCoordinatorFeeAmount > 0) + { + qpi.transfer(state.get().takeoverCoordinator, locals.takeoverCoordinatorFeeAmount); + } + + 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; + } + + output.success = 1; + } + PRIVATE_FUNCTION_WITH_LOCALS(CountAllowedBidderWallets) { output.allowedWalletCount = 0; - for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < NOST_AUCTION_ALLOWED_WALLET_NUM; ++locals.allowedWalletIndex) + for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < input.allowedBidderWallets.capacity(); ++locals.allowedWalletIndex) { if (!isZero(input.allowedBidderWallets.get(locals.allowedWalletIndex))) { @@ -1222,7 +1384,7 @@ struct NOST : public ContractBase return; } - for (locals.cidIndex = 1; locals.cidIndex < NOST_AUCTION_METADATA_CID_LENGTH; ++locals.cidIndex) + for (locals.cidIndex = 1; locals.cidIndex < input.metadataIpfsCid.capacity(); ++locals.cidIndex) { locals.cidChar = input.metadataIpfsCid.get(locals.cidIndex); if (locals.cidChar == 0) @@ -1256,7 +1418,7 @@ struct NOST : public ContractBase PRIVATE_FUNCTION_WITH_LOCALS(VerifyAuctionLotBalances) { output.hasEnoughBalance = 1; - for (locals.lotItemIndex = 0; locals.lotItemIndex < NOST_AUCTION_LOT_ITEM_NUM; ++locals.lotItemIndex) + 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) @@ -1276,7 +1438,7 @@ struct NOST : public ContractBase PRIVATE_PROCEDURE_WITH_LOCALS(RollbackAuctionLotAssets) { - for (locals.lotItemIndex = 0; locals.lotItemIndex < NOST_AUCTION_LOT_ITEM_NUM; ++locals.lotItemIndex) + 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) @@ -1294,6 +1456,7 @@ struct NOST : public ContractBase locals.bestParticipantFound = 0; locals.lotItemFound = 0; locals.soldQuantity = 0; + locals.totalGrossAmount = 0; if (!state.get().auctionList.get(input.auctionId, locals.auction)) { @@ -1305,7 +1468,7 @@ struct NOST : public ContractBase return; } - for (locals.lotItemIndex = 0; locals.lotItemIndex < NOST_AUCTION_LOT_ITEM_NUM; ++locals.lotItemIndex) + for (locals.lotItemIndex = 0; locals.lotItemIndex < locals.auction.auctionLotItems.capacity(); ++locals.lotItemIndex) { locals.batchLotItem = locals.auction.auctionLotItems.get(locals.lotItemIndex); if (!isZeroAsset(locals.batchLotItem.asset) && locals.batchLotItem.quantity > 0) @@ -1360,12 +1523,12 @@ struct NOST : public ContractBase if (locals.allocatedQuantity > 0) { - qpi.transfer(locals.auction.seller, locals.requiredPayment); 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; } @@ -1403,6 +1566,13 @@ struct NOST : public ContractBase locals.auction.quantityForSale - locals.soldQuantity, locals.auction.seller); } + locals.distributeAuctionRevenueInput.grossAmount = locals.totalGrossAmount; + CALL(DistributeAuctionRevenue, locals.distributeAuctionRevenueInput, locals.distributeAuctionRevenueOutput); + if (locals.distributeAuctionRevenueOutput.sellerPayout > 0) + { + qpi.transfer(locals.auction.seller, locals.distributeAuctionRevenueOutput.sellerPayout); + } + locals.auction.allocatedQuantity = locals.soldQuantity; locals.auction.status = EAuctionStatus::Finalized; locals.auction.settledAt = input.currentDate; @@ -1434,11 +1604,17 @@ struct NOST : public ContractBase if (locals.highestBidderExists && locals.highestBidderData.escrowedAmount > 0) { - qpi.transfer(locals.auction.seller, locals.highestBidderData.escrowedAmount); locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.auctionLotItems; locals.rollbackAuctionLotAssetsInput.recipient = locals.highestBidderData.participant; CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); + locals.distributeAuctionRevenueInput.grossAmount = locals.highestBidderData.escrowedAmount; + CALL(DistributeAuctionRevenue, locals.distributeAuctionRevenueInput, locals.distributeAuctionRevenueOutput); + if (locals.distributeAuctionRevenueOutput.sellerPayout > 0) + { + qpi.transfer(locals.auction.seller, locals.distributeAuctionRevenueOutput.sellerPayout); + } + locals.highestBidderData.allocatedQuantity = locals.auction.quantityForSale; locals.highestBidderData.isWinningBid = 1; locals.highestBidderData.escrowedAmount = 0; @@ -1467,10 +1643,57 @@ struct NOST : public ContractBase output.success = 1; } + PRIVATE_PROCEDURE_WITH_LOCALS(RejectStandardAuction) + { + output.refundedAmount = 0; + output.success = 0; + locals.highestBidderExists = 0; + + if (!state.get().auctionList.get(input.auctionId, locals.auction)) + { + return; + } + + if (locals.auction.type != EAuctionType::Standard || locals.auction.status != EAuctionStatus::PendingSellerDecision) + { + return; + } + + if (!isZero(locals.auction.highestBidder)) + { + locals.highestBidderKey = {locals.auction.auctionId, locals.auction.highestBidder}; + locals.highestBidderExists = state.get().participants.get(locals.highestBidderKey, locals.highestBidderData); + } + + if (locals.highestBidderExists && locals.highestBidderData.escrowedAmount > 0) + { + qpi.transfer(locals.highestBidderData.participant, locals.highestBidderData.escrowedAmount); + output.refundedAmount = locals.highestBidderData.escrowedAmount; + locals.highestBidderData.escrowedAmount = 0; + locals.highestBidderData.allocatedQuantity = 0; + locals.highestBidderData.isWinningBid = 0; + state.mut().participants.replace(locals.highestBidderKey, locals.highestBidderData); + } + + locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.seller; + CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); + + locals.auction.allocatedQuantity = 0; + locals.auction.highestBidAmount = 0; + locals.auction.highestBidPrice = 0; + locals.auction.highestBidQuantity = 0; + locals.auction.highestBidder = NULL_ID; + locals.auction.status = EAuctionStatus::Finalized; + locals.auction.settledAt = input.currentDate; + state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); + output.success = 1; + } + PRIVATE_PROCEDURE_WITH_LOCALS(EscrowAuctionLotAssets) { output.success = 1; - for (locals.lotItemIndex = 0; locals.lotItemIndex < NOST_AUCTION_LOT_ITEM_NUM; ++locals.lotItemIndex) + 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) @@ -1678,7 +1901,7 @@ struct NOST : public ContractBase } } locals.auction.auctionLotItems = input.auctionLotItems; - for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < NOST_AUCTION_ALLOWED_WALLET_NUM; ++locals.allowedWalletIndex) + for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < input.allowedBidderWallets.capacity(); ++locals.allowedWalletIndex) { if (!isZero(input.allowedBidderWallets.get(locals.allowedWalletIndex))) { @@ -1930,6 +2153,69 @@ struct NOST : public ContractBase output.errorCode = static_cast(EAuctionError::Success); } + PUBLIC_PROCEDURE_WITH_LOCALS(ResolvePendingStandardAuction) + { + output.refundedAmount = 0; + output.errorCode = static_cast(EAuctionError::InvalidInput); + + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (input.acceptSale > 1) + { + return; + } + + if (!state.get().auctionList.get(input.auctionId, locals.auction)) + { + output.errorCode = static_cast(EAuctionError::AuctionNotFound); + return; + } + + if (locals.auction.seller != qpi.invocator()) + { + output.errorCode = static_cast(EAuctionError::Forbidden); + return; + } + + if (locals.auction.type != EAuctionType::Standard || locals.auction.status != EAuctionStatus::PendingSellerDecision) + { + output.errorCode = static_cast(EAuctionError::AuctionClosed); + return; + } + + locals.currentDate = qpi.now(); + if (locals.auction.sellerDecisionDeadline <= locals.currentDate) + { + locals.finalizeStandardAuctionInput.auctionId = input.auctionId; + locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); + + output.errorCode = static_cast(EAuctionError::AuctionClosed); + return; + } + + if (input.acceptSale) + { + locals.finalizeStandardAuctionInput.auctionId = input.auctionId; + locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); + output.errorCode = locals.finalizeStandardAuctionOutput.success ? static_cast(EAuctionError::Success) + : static_cast(EAuctionError::AuctionClosed); + } + else + { + locals.rejectStandardAuctionInput.auctionId = input.auctionId; + locals.rejectStandardAuctionInput.currentDate = locals.currentDate; + CALL(RejectStandardAuction, locals.rejectStandardAuctionInput, locals.rejectStandardAuctionOutput); + output.refundedAmount = locals.rejectStandardAuctionOutput.refundedAmount; + output.errorCode = locals.rejectStandardAuctionOutput.success ? static_cast(EAuctionError::Success) + : static_cast(EAuctionError::AuctionClosed); + } + } + PUBLIC_FUNCTION(GetAuction) { state.get().auctionList.get(input.auctionId, output.auction); } PUBLIC_FUNCTION(GetAuctionParticipant) @@ -2040,6 +2326,23 @@ struct NOST : public ContractBase return visibility != EAuctionVisibility::Private || ((requiredAccessAssetCount > 0) != (allowedWalletCount > 0)); } + static uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount) + { + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1) + { + return NOST_AUCTION_SHAREHOLDER_FEE_BP_TIER_1; + } + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2) + { + return NOST_AUCTION_SHAREHOLDER_FEE_BP_TIER_2; + } + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3) + { + return NOST_AUCTION_SHAREHOLDER_FEE_BP_TIER_3; + } + return NOST_AUCTION_SHAREHOLDER_FEE_BP_TIER_4; + } + static sint64 getCreateAuctionFee(EAuctionVisibility visibility, const ContractState& state) { return visibility == EAuctionVisibility::Private ? state.get().privateAuctionFee : 0; From 737dd6a45968d347c9bfd7e367504e028e40cfd9 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 16 Apr 2026 19:08:16 +0300 Subject: [PATCH 19/59] Expand auction handling in `Nostromo`: add structured input/output for auction procedures, refine field documentation, and introduce detailed `@brief` comments for clarity and consistency. --- src/contracts/Nostromo.h | 73 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 94b7bf511..d8c71b5bf 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -393,12 +393,13 @@ struct NOST : public ContractBase uint8 found; }; - struct GetTicksBeforeAuctionLaunch_input - { - }; + /** @brief Input payload used to query the remaining post-BEGIN_EPOCH auction launch pause. */ + typedef NoData GetTicksBeforeAuctionLaunch_input; + /** @brief Result returned by the auction launch pause getter. */ struct GetTicksBeforeAuctionLaunch_output { + /** @brief Number of ticks remaining before auction interactions resume after `BEGIN_EPOCH`. */ uint32 ticks; }; @@ -491,66 +492,98 @@ struct NOST : public ContractBase 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 Identifier of the batch auction to finalize. */ id auctionId; }; + /** @brief Internal output returned after batch auction finalization. */ struct FinalizeBatchAuction_output { + /** @brief Flag indicating whether batch settlement finished successfully. */ 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 Identifier of the standard auction to finalize. */ id auctionId; }; + /** @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 Identifier of the pending standard auction to reject. */ id auctionId; }; + /** @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 the 30-minute pre-epoch pause or 500-tick post-BEGIN_EPOCH pause is active. */ uint8 isPaused; }; + /** @brief Internal input used to split auction proceeds between seller and configured fee recipients. */ struct DistributeAuctionRevenue_input { + /** @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 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; }; @@ -1726,6 +1759,11 @@ struct NOST : public ContractBase } } + /** + * @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 Private auctions require the configured private auction fee and must use exactly one access mode. + */ PUBLIC_PROCEDURE_WITH_LOCALS(CreateAuction) { output.errorCode = static_cast(EAuctionError::InvalidInput); @@ -1935,6 +1973,11 @@ struct NOST : public ContractBase output.errorCode = static_cast(EAuctionError::Success); } + /** + * @brief Places a bid in an active auction. + * @note Batch auctions interpret `bidAmount` as price per asset and `quantity` as the requested amount. + * @note Standard auctions interpret `bidAmount` as the total price for the whole lot and ignore `quantity`. + */ PUBLIC_PROCEDURE_WITH_LOCALS(PlaceBid) { output.errorCode = static_cast(EAuctionError::InvalidInput); @@ -2066,6 +2109,10 @@ struct NOST : public ContractBase output.errorCode = static_cast(EAuctionError::Success); } + /** + * @brief Cancels an active auction and refunds every escrowed bid. + * @note The cancellation fee is based on the current highest bid or on the configured reserve price for the full batch quantity or standard lot. + */ PUBLIC_PROCEDURE_WITH_LOCALS(CancelAuction) { output.errorCode = static_cast(EAuctionError::InvalidInput); @@ -2153,6 +2200,10 @@ struct NOST : public ContractBase output.errorCode = static_cast(EAuctionError::Success); } + /** + * @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; @@ -2216,13 +2267,25 @@ struct NOST : public ContractBase } } + /** + * @brief Returns the stored state of one auction. + * @note The response contains the full persistent `AuctionData` record for the requested auction identifier. + */ PUBLIC_FUNCTION(GetAuction) { state.get().auctionList.get(input.auctionId, output.auction); } + /** + * @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(GetAuctionParticipant) { output.found = state.get().participants.get({input.auctionId, input.participant}, output.participantData) ? 1 : 0; } + /** + * @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(GetTicksBeforeAuctionLaunch) { output.ticks = 0; @@ -2237,6 +2300,10 @@ struct NOST : public ContractBase 0)); } + /** + * @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. + */ PUBLIC_PROCEDURE(TransferShareManagementRights) { if (qpi.invocationReward() > 0) From 2bd718d134ce58f63e3cf46c676afc6debf1de7b Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 16 Apr 2026 19:22:25 +0300 Subject: [PATCH 20/59] Refactor auction type handling in `Nostromo`: replace conditional logic with `switch` statement and improve inline documentation for batch auction finalization procedures. --- src/contracts/Nostromo.h | 57 +++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index d8c71b5bf..c27765e34 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -1007,28 +1007,30 @@ struct NOST : public ContractBase diffDateInSecond(locals.auction.createdAt, locals.currentDate, locals.elapsedSeconds); if (locals.elapsedSeconds >= locals.auction.auctionDurationSeconds) { - if (locals.auction.type == EAuctionType::Batch) + switch (locals.auction.type) { - locals.finalizeBatchAuctionInput.auctionId = locals.auction.auctionId; - locals.finalizeBatchAuctionInput.currentDate = locals.currentDate; - CALL(FinalizeBatchAuction, locals.finalizeBatchAuctionInput, locals.finalizeBatchAuctionOutput); - } - else if (locals.auction.type == EAuctionType::Standard) - { - if (locals.auction.highestBidAmount == 0 || locals.auction.highestBidPrice >= locals.auction.salePrice) - { - locals.finalizeStandardAuctionInput.auctionId = locals.auction.auctionId; - locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; - CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); - } - else - { - locals.auction.status = EAuctionStatus::PendingSellerDecision; - locals.auction.sellerDecisionDeadline = locals.currentDate; - locals.auction.sellerDecisionDeadline.add(0, 0, 0, 0, 0, NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS); - state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); - } - } + case EAuctionType::Batch: + locals.finalizeBatchAuctionInput.auctionId = locals.auction.auctionId; + locals.finalizeBatchAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeBatchAuction, locals.finalizeBatchAuctionInput, locals.finalizeBatchAuctionOutput); + break; + case EAuctionType::Standard: + if (locals.auction.highestBidAmount == 0 || locals.auction.highestBidPrice >= locals.auction.salePrice) + { + locals.finalizeStandardAuctionInput.auctionId = locals.auction.auctionId; + locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; + CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); + } + else + { + locals.auction.status = EAuctionStatus::PendingSellerDecision; + locals.auction.sellerDecisionDeadline = locals.currentDate; + locals.auction.sellerDecisionDeadline.add(0, 0, 0, 0, 0, NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS); + state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); + } + break; + default: break; + }; } } else if (locals.auction.status == EAuctionStatus::PendingSellerDecision && locals.auction.sellerDecisionDeadline <= locals.currentDate) @@ -1491,6 +1493,7 @@ struct NOST : public ContractBase 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.auctionId, locals.auction)) { return; @@ -1501,6 +1504,7 @@ struct NOST : public ContractBase return; } + // Resolve the single sellable lot entry that represents the batch asset and quantity in escrow. for (locals.lotItemIndex = 0; locals.lotItemIndex < locals.auction.auctionLotItems.capacity(); ++locals.lotItemIndex) { locals.batchLotItem = locals.auction.auctionLotItems.get(locals.lotItemIndex); @@ -1515,11 +1519,14 @@ struct NOST : public ContractBase return; } + // Repeatedly pick the best remaining bid, allocate available quantity, and collect the winning payment. locals.remainingQuantity = locals.auction.quantityForSale; while (locals.remainingQuantity > 0) { locals.bestParticipantFound = 0; locals.participantIndex = state.get().participants.nextElementIndex(NULL_INDEX); + + // Scan all bids for this auction to find the highest price, using earlier bid time as the tie-breaker. while (locals.participantIndex != NULL_INDEX) { locals.participantKey = state.get().participants.key(locals.participantIndex); @@ -1546,6 +1553,7 @@ struct NOST : public ContractBase 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; @@ -1554,6 +1562,7 @@ struct NOST : public ContractBase 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, @@ -1565,15 +1574,18 @@ struct NOST : public ContractBase 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) { qpi.transfer(locals.bestParticipantData.participant, locals.refundAmount); } + // Clear the processed escrow so the same bid cannot participate in later iterations. locals.bestParticipantData.escrowedAmount = 0; state.mut().participants.replace(locals.bestParticipantKey, locals.bestParticipantData); } + // Refund every non-winning or non-allocated bid that still has escrow locked after winner selection. locals.participantIndex = state.get().participants.nextElementIndex(NULL_INDEX); while (locals.participantIndex != NULL_INDEX) { @@ -1593,12 +1605,14 @@ struct NOST : public ContractBase locals.participantIndex = state.get().participants.nextElementIndex(locals.participantIndex); } + // Return any unsold batch quantity to the seller when demand did not consume the entire lot. if (locals.soldQuantity < locals.auction.quantityForSale) { qpi.transferShareOwnershipAndPossession(locals.batchLotItem.asset.assetName, locals.batchLotItem.asset.issuer, SELF, SELF, locals.auction.quantityForSale - locals.soldQuantity, locals.auction.seller); } + // Split the collected proceeds according to Nostromo auction fee rules and pay the seller net amount. locals.distributeAuctionRevenueInput.grossAmount = locals.totalGrossAmount; CALL(DistributeAuctionRevenue, locals.distributeAuctionRevenueInput, locals.distributeAuctionRevenueOutput); if (locals.distributeAuctionRevenueOutput.sellerPayout > 0) @@ -1606,6 +1620,7 @@ struct NOST : public ContractBase qpi.transfer(locals.auction.seller, locals.distributeAuctionRevenueOutput.sellerPayout); } + // Persist the final sold quantity and close the auction as settled. locals.auction.allocatedQuantity = locals.soldQuantity; locals.auction.status = EAuctionStatus::Finalized; locals.auction.settledAt = input.currentDate; From ef24db50972c541e2728e6f3f3f4de7aca62f470 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 16 Apr 2026 20:52:45 +0300 Subject: [PATCH 21/59] Add highest bid recomputation for batch auctions in `Nostromo`: implement `RecomputeBatchHighestBid` procedure, update bid processing to trigger recomputation when needed, and introduce supporting data structures and logic. --- src/contracts/Nostromo.h | 87 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index c27765e34..77f99e108 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -606,6 +606,15 @@ struct NOST : public ContractBase uint64 elapsedSeconds; }; + /** @brief Internal input used to refresh the cached highest bid fields of one batch auction. */ + struct RecomputeBatchHighestBid_input + { + /** @brief Identifier of the batch auction whose cached top bid must be rebuilt. */ + id auctionId; + }; + + typedef NoData RecomputeBatchHighestBid_output; + /** @brief Internal output returned after processing a batch auction bid. */ struct ProcessBatchBid_output { @@ -627,11 +636,25 @@ struct NOST : public ContractBase AuctionData auction; AuctionParticipantData participantData; AuctionParticipantKey participantKey; + RecomputeBatchHighestBid_input recomputeBatchHighestBidInput; + RecomputeBatchHighestBid_output recomputeBatchHighestBidOutput; uint64 previousEscrow; uint64 requiredEscrow; + uint8 mustRecomputeHighestBid; uint8 participantExists; }; + struct RecomputeBatchHighestBid_locals + { + AuctionData auction; + AuctionParticipantData participantData; + AuctionParticipantData bestParticipantData; + AuctionParticipantKey participantKey; + AuctionParticipantKey bestParticipantKey; + sint64 participantIndex; + uint8 bestParticipantFound; + }; + /** @brief Internal input used to process a standard auction bid after the common PlaceBid checks succeed. */ struct ProcessStandardBid_input { @@ -1212,6 +1235,63 @@ struct NOST : public ContractBase } } + PRIVATE_PROCEDURE_WITH_LOCALS(RecomputeBatchHighestBid) + { + locals.bestParticipantFound = 0; + + if (!state.get().auctionList.get(input.auctionId, locals.auction)) + { + return; + } + + if (locals.auction.type != EAuctionType::Batch) + { + return; + } + + for (locals.participantIndex = state.get().participants.nextElementIndex(NULL_INDEX); locals.participantIndex != NULL_INDEX; + locals.participantIndex = state.get().participants.nextElementIndex(locals.participantIndex)) + { + locals.participantKey = state.get().participants.key(locals.participantIndex); + if (locals.participantKey.auctionId != input.auctionId) + { + continue; + } + + locals.participantData = state.get().participants.value(locals.participantIndex); + if (locals.participantData.escrowedAmount == 0) + { + continue; + } + + if (!locals.bestParticipantFound || locals.participantData.bidAmount > locals.bestParticipantData.bidAmount || + (locals.participantData.bidAmount == locals.bestParticipantData.bidAmount && + locals.participantData.lastBidTime < locals.bestParticipantData.lastBidTime)) + { + locals.bestParticipantFound = 1; + locals.bestParticipantData = locals.participantData; + locals.bestParticipantKey = locals.participantKey; + } + } + + if (locals.bestParticipantFound) + { + locals.auction.highestBidder = locals.bestParticipantKey.participant; + locals.auction.highestBidPrice = locals.bestParticipantData.bidAmount; + locals.auction.highestBidQuantity = locals.bestParticipantData.requestedQuantity; + locals.auction.highestBidAmount = locals.bestParticipantData.escrowedAmount; + } + else + { + locals.auction.highestBidAmount = 0; + locals.auction.highestBidPrice = 0; + locals.auction.highestBidQuantity = 0; + locals.auction.highestBidder = NULL_ID; + } + + state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); + } + PRIVATE_PROCEDURE_WITH_LOCALS(ProcessBatchBid) { output.escrowedAmount = 0; @@ -1246,6 +1326,8 @@ struct NOST : public ContractBase locals.participantKey = {input.auctionId, qpi.invocator()}; locals.participantExists = state.get().participants.get(locals.participantKey, locals.participantData); locals.previousEscrow = locals.participantExists ? locals.participantData.escrowedAmount : 0; + locals.mustRecomputeHighestBid = locals.participantExists && locals.auction.highestBidder == qpi.invocator() && + input.bidAmount <= locals.auction.highestBidPrice; locals.participantData.escrowedAmount = locals.requiredEscrow; locals.participantData.requestedQuantity = input.effectiveQuantity; @@ -1275,6 +1357,11 @@ struct NOST : public ContractBase return; } state.mut().auctionList.replace(input.auctionId, locals.auction); + if (locals.mustRecomputeHighestBid) + { + locals.recomputeBatchHighestBidInput.auctionId = input.auctionId; + CALL(RecomputeBatchHighestBid, locals.recomputeBatchHighestBidInput, locals.recomputeBatchHighestBidOutput); + } if (locals.previousEscrow > 0) { From bac938ebf87c93be9d27f6931eafae4baae508e0 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 16 Apr 2026 21:05:36 +0300 Subject: [PATCH 22/59] Add dynamic auction fee configuration in `Nostromo`: introduce default fee constants, enable runtime fee updates via new procedures `SetAuctionFees`, ` --- src/contracts/Nostromo.h | 309 +++++++++++++++++++++++++++++++++++---- 1 file changed, 282 insertions(+), 27 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 77f99e108..372ac7846 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -15,16 +15,16 @@ constexpr uint64 NOST_AUCTION_LOT_ITEM_NUM = 64; constexpr uint64 NOST_AUCTION_ALLOWED_WALLET_NUM = 128; constexpr uint64 NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM = 16; constexpr uint32 NOST_AUCTION_MAX_DURATION_DAYS = 30; -constexpr sint64 NOST_PRIVATE_AUCTION_FEE = 50000000LL; -constexpr uint64 NOST_AUCTION_CANCELLATION_FEE_BP = 1000ULL; -constexpr uint64 NOST_AUCTION_MANAGEMENT_FEE_BP = 50ULL; -constexpr uint64 NOST_AUCTION_DEVELOPMENT_FEE_BP = 50ULL; -constexpr uint64 NOST_AUCTION_TAKEOVER_COORDINATOR_FEE_BP = 50ULL; -constexpr uint64 NOST_AUCTION_SHAREHOLDER_DIVIDEND_BP = 9000ULL; -constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_BP_TIER_1 = 500ULL; -constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_BP_TIER_2 = 450ULL; -constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_BP_TIER_3 = 400ULL; -constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_BP_TIER_4 = 350ULL; +constexpr sint64 NOST_DEFAULT_PRIVATE_AUCTION_FEE = 50000000LL; +constexpr uint64 NOST_DEFAULT_AUCTION_CANCELLATION_FEE_BP = 1000ULL; +constexpr uint64 NOST_DEFAULT_AUCTION_MANAGEMENT_FEE_BP = 50ULL; +constexpr uint64 NOST_DEFAULT_AUCTION_DEVELOPMENT_FEE_BP = 50ULL; +constexpr uint64 NOST_DEFAULT_AUCTION_TAKEOVER_COORDINATOR_FEE_BP = 50ULL; +constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_DIVIDEND_BP = 9000ULL; +constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_1 = 500ULL; +constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_2 = 450ULL; +constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_3 = 400ULL; +constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4 = 350ULL; constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1 = 5000000000ULL; constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2 = 50000000000ULL; constexpr uint64 NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3 = 200000000000ULL; @@ -226,6 +226,30 @@ struct NOST : public ContractBase /** @brief Undistributed auction shareholder revenue 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 Configured maximum auction duration in days. */ uint32 maxAuctionDurationDays; @@ -359,6 +383,93 @@ struct NOST : public ContractBase uint8 errorCode; }; + /** @brief Input payload used by the takeover coordinator to overwrite the full auction fee configuration. */ + struct SetAuctionFees_input + { + /** @brief Fee charged when a private auction is created. */ + sint64 privateAuctionFee; + + /** @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; + }; + + struct SetAuctionFees_output + { + /** @brief Result code describing whether the fee update succeeded. */ + uint8 errorCode; + }; + + /** @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 Cancellation fee rate in basis points. */ + uint64 auctionCancellationFeeBasisPoints; + + /** @brief Management fee rate in basis points. */ + uint64 managementFeeBasisPoints; + + /** @brief Development fee rate in basis points. */ + uint64 developmentFeeBasisPoints; + + /** @brief Shareholder fee tier for auctions up to the first threshold. */ + uint64 shareholderFeeBasisPointsTier1; + + /** @brief Shareholder fee tier for auctions above the first threshold and up to the second threshold. */ + uint64 shareholderFeeBasisPointsTier2; + + /** @brief Shareholder fee tier for auctions above the second threshold and up to the third threshold. */ + uint64 shareholderFeeBasisPointsTier3; + + /** @brief Shareholder fee tier for auctions above the third threshold. */ + uint64 shareholderFeeBasisPointsTier4; + }; + + struct SetAuctionFeesByManagement_output + { + /** @brief Result code describing whether the fee update succeeded. */ + uint8 errorCode; + }; + + /** @brief Input payload used by the takeover coordinator to appoint a new management wallet. */ + struct SetManagement_input + { + /** @brief New wallet that will receive management privileges. */ + id management; + }; + + struct SetManagement_output + { + /** @brief Result code describing whether the management update succeeded. */ + uint8 errorCode; + }; + /** @brief Input payload used to fetch one auction from storage. */ struct GetAuction_input { @@ -949,6 +1060,9 @@ struct NOST : public ContractBase REGISTER_USER_PROCEDURE(CancelAuction, 3); REGISTER_USER_PROCEDURE(TransferShareManagementRights, 4); REGISTER_USER_PROCEDURE(ResolvePendingStandardAuction, 5); + REGISTER_USER_PROCEDURE(SetAuctionFees, 6); + REGISTER_USER_PROCEDURE(SetAuctionFeesByManagement, 7); + REGISTER_USER_PROCEDURE(SetManagement, 8); REGISTER_USER_FUNCTION(GetAuction, 1); REGISTER_USER_FUNCTION(GetAuctionParticipant, 2); @@ -957,8 +1071,16 @@ struct NOST : public ContractBase INITIALIZE() { - state.mut().privateAuctionFee = NOST_PRIVATE_AUCTION_FEE; - state.mut().auctionCancellationFeeBasisPoints = NOST_AUCTION_CANCELLATION_FEE_BP; + state.mut().privateAuctionFee = NOST_DEFAULT_PRIVATE_AUCTION_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().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); @@ -981,8 +1103,16 @@ struct NOST : public ContractBase if (qpi.epoch() == 220) { // Initialize - state.mut().privateAuctionFee = NOST_PRIVATE_AUCTION_FEE; - state.mut().auctionCancellationFeeBasisPoints = NOST_AUCTION_CANCELLATION_FEE_BP; + state.mut().privateAuctionFee = NOST_DEFAULT_PRIVATE_AUCTION_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().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, @@ -1157,15 +1287,15 @@ struct NOST : public ContractBase return; } - locals.shareholderFeeBasisPoints = getAuctionShareholderFeeBasisPoints(input.grossAmount); + locals.shareholderFeeBasisPoints = getAuctionShareholderFeeBasisPoints(input.grossAmount, state); locals.shareholderFeeAmount = div(smul(input.grossAmount, locals.shareholderFeeBasisPoints), 10000ULL); - locals.managementFeeAmount = div(smul(input.grossAmount, NOST_AUCTION_MANAGEMENT_FEE_BP), 10000ULL); - locals.developmentFeeAmount = div(smul(input.grossAmount, NOST_AUCTION_DEVELOPMENT_FEE_BP), 10000ULL); - locals.shareholderDividendAmount = div(smul(locals.shareholderFeeAmount, NOST_AUCTION_SHAREHOLDER_DIVIDEND_BP), 10000ULL); - locals.takeoverCoordinatorFeeAmount = div(smul(input.grossAmount, NOST_AUCTION_TAKEOVER_COORDINATOR_FEE_BP), 10000ULL) + + locals.managementFeeAmount = div(smul(input.grossAmount, state.get().managementFeeBasisPoints), 10000ULL); + locals.developmentFeeAmount = div(smul(input.grossAmount, state.get().developmentFeeBasisPoints), 10000ULL); + locals.shareholderDividendAmount = div(smul(locals.shareholderFeeAmount, state.get().shareholderDividendBasisPoints), 10000ULL); + locals.takeoverCoordinatorFeeAmount = div(smul(input.grossAmount, state.get().takeoverCoordinatorFeeBasisPoints), 10000ULL) + (locals.shareholderFeeAmount - locals.shareholderDividendAmount); output.sellerPayout = input.grossAmount - locals.shareholderFeeAmount - locals.managementFeeAmount - locals.developmentFeeAmount - - div(smul(input.grossAmount, NOST_AUCTION_TAKEOVER_COORDINATOR_FEE_BP), 10000ULL); + div(smul(input.grossAmount, state.get().takeoverCoordinatorFeeBasisPoints), 10000ULL); state.mut().auctionShareholderDividendPool = sadd(state.get().auctionShareholderDividendPool, locals.shareholderDividendAmount); if (locals.managementFeeAmount > 0) @@ -1326,8 +1456,8 @@ struct NOST : public ContractBase locals.participantKey = {input.auctionId, qpi.invocator()}; locals.participantExists = state.get().participants.get(locals.participantKey, locals.participantData); locals.previousEscrow = locals.participantExists ? locals.participantData.escrowedAmount : 0; - locals.mustRecomputeHighestBid = locals.participantExists && locals.auction.highestBidder == qpi.invocator() && - input.bidAmount <= locals.auction.highestBidPrice; + locals.mustRecomputeHighestBid = + locals.participantExists && locals.auction.highestBidder == qpi.invocator() && input.bidAmount <= locals.auction.highestBidPrice; locals.participantData.escrowedAmount = locals.requiredEscrow; locals.participantData.requestedQuantity = input.effectiveQuantity; @@ -2369,6 +2499,111 @@ struct NOST : public ContractBase } } + /** + * @brief Overwrites the full auction fee configuration. + * @note Only the configured takeover coordinator can call this procedure. + */ + PUBLIC_PROCEDURE(SetAuctionFees) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.get().takeoverCoordinator) + { + output.errorCode = static_cast(EAuctionError::Forbidden); + return; + } + + if (!isValidAuctionFeeConfiguration( + input.privateAuctionFee, input.auctionCancellationFeeBasisPoints, input.managementFeeBasisPoints, input.developmentFeeBasisPoints, + input.takeoverCoordinatorFeeBasisPoints, input.shareholderDividendBasisPoints, input.shareholderFeeBasisPointsTier1, + input.shareholderFeeBasisPointsTier2, input.shareholderFeeBasisPointsTier3, input.shareholderFeeBasisPointsTier4)) + { + output.errorCode = static_cast(EAuctionError::InvalidInput); + return; + } + + state.mut().privateAuctionFee = input.privateAuctionFee; + 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 = static_cast(EAuctionError::Success); + } + + /** + * @brief Updates every auction fee except the takeover coordinator-specific splits. + * @note Only the configured management wallet can call this procedure. + */ + PUBLIC_PROCEDURE(SetAuctionFeesByManagement) + { + output.errorCode = static_cast(EAuctionError::InvalidInput); + + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.get().management) + { + output.errorCode = static_cast(EAuctionError::Forbidden); + return; + } + + if (!isValidAuctionFeeConfiguration( + input.privateAuctionFee, input.auctionCancellationFeeBasisPoints, input.managementFeeBasisPoints, input.developmentFeeBasisPoints, + state.get().takeoverCoordinatorFeeBasisPoints, state.get().shareholderDividendBasisPoints, input.shareholderFeeBasisPointsTier1, + input.shareholderFeeBasisPointsTier2, input.shareholderFeeBasisPointsTier3, input.shareholderFeeBasisPointsTier4)) + { + return; + } + + state.mut().privateAuctionFee = input.privateAuctionFee; + 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 = static_cast(EAuctionError::Success); + } + + /** + * @brief Reassigns the management role to another wallet. + * @note Only the configured takeover coordinator can call this procedure. + */ + PUBLIC_PROCEDURE(SetManagement) + { + output.errorCode = static_cast(EAuctionError::InvalidInput); + + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.get().takeoverCoordinator) + { + output.errorCode = static_cast(EAuctionError::Forbidden); + return; + } + + if (isZero(input.management)) + { + return; + } + + state.mut().management = input.management; + output.errorCode = static_cast(EAuctionError::Success); + } + /** * @brief Returns the stored state of one auction. * @note The response contains the full persistent `AuctionData` record for the requested auction identifier. @@ -2495,21 +2730,41 @@ struct NOST : public ContractBase return visibility != EAuctionVisibility::Private || ((requiredAccessAssetCount > 0) != (allowedWalletCount > 0)); } - static uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount) + static bool isValidAuctionFeeConfiguration(sint64 privateAuctionFee, uint64 auctionCancellationFeeBasisPoints, uint64 managementFeeBasisPoints, + uint64 developmentFeeBasisPoints, uint64 takeoverCoordinatorFeeBasisPoints, + uint64 shareholderDividendBasisPoints, uint64 shareholderFeeBasisPointsTier1, + uint64 shareholderFeeBasisPointsTier2, uint64 shareholderFeeBasisPointsTier3, + uint64 shareholderFeeBasisPointsTier4) + { + return privateAuctionFee >= 0 && auctionCancellationFeeBasisPoints <= 10000ULL && managementFeeBasisPoints <= 10000ULL && + developmentFeeBasisPoints <= 10000ULL && takeoverCoordinatorFeeBasisPoints <= 10000ULL && shareholderDividendBasisPoints <= 10000ULL && + shareholderFeeBasisPointsTier1 <= 10000ULL && shareholderFeeBasisPointsTier2 <= 10000ULL && + shareholderFeeBasisPointsTier3 <= 10000ULL && shareholderFeeBasisPointsTier4 <= 10000ULL && + (shareholderFeeBasisPointsTier1 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= + 10000ULL && + (shareholderFeeBasisPointsTier2 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= + 10000ULL && + (shareholderFeeBasisPointsTier3 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= + 10000ULL && + (shareholderFeeBasisPointsTier4 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= + 10000ULL; + } + + static uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount, const ContractState& state) { if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1) { - return NOST_AUCTION_SHAREHOLDER_FEE_BP_TIER_1; + return state.get().shareholderFeeBasisPointsTier1; } if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2) { - return NOST_AUCTION_SHAREHOLDER_FEE_BP_TIER_2; + return state.get().shareholderFeeBasisPointsTier2; } if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3) { - return NOST_AUCTION_SHAREHOLDER_FEE_BP_TIER_3; + return state.get().shareholderFeeBasisPointsTier3; } - return NOST_AUCTION_SHAREHOLDER_FEE_BP_TIER_4; + return state.get().shareholderFeeBasisPointsTier4; } static sint64 getCreateAuctionFee(EAuctionVisibility visibility, const ContractState& state) From 6db2e47cc90d86418bb4c30817c8dcbb8926bb70 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 16 Apr 2026 21:14:47 +0300 Subject: [PATCH 23/59] Add `GetAuctionFees` and `GetFeeRecipients` procedures in `Nostromo`: expose current fee configuration and recipient wallets via structured input/output, register new user functions. --- src/contracts/Nostromo.h | 88 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 372ac7846..bb75d23d6 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -505,7 +505,7 @@ struct NOST : public ContractBase }; /** @brief Input payload used to query the remaining post-BEGIN_EPOCH auction launch pause. */ - typedef NoData GetTicksBeforeAuctionLaunch_input; + using GetTicksBeforeAuctionLaunch_input = NoData; /** @brief Result returned by the auction launch pause getter. */ struct GetTicksBeforeAuctionLaunch_output @@ -514,6 +514,57 @@ struct NOST : public ContractBase uint32 ticks; }; + /** @brief Input payload used to read the current auction fee configuration. */ + using GetAuctionFees_input = NoData; + + struct GetAuctionFees_output + { + /** @brief Fee charged when a private auction is created. */ + sint64 privateAuctionFee; + + /** @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; + }; + + /** @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 Internal input used to validate an auction lot and resolve its total escrow quantity. */ struct AnalyzeAuctionLot_input { @@ -724,7 +775,7 @@ struct NOST : public ContractBase id auctionId; }; - typedef NoData RecomputeBatchHighestBid_output; + using RecomputeBatchHighestBid_output = NoData; /** @brief Internal output returned after processing a batch auction bid. */ struct ProcessBatchBid_output @@ -889,7 +940,7 @@ struct NOST : public ContractBase id recipient; }; - typedef NoData RollbackAuctionLotAssets_output; + using RollbackAuctionLotAssets_output = NoData; struct RollbackAuctionLotAssets_locals { @@ -1067,6 +1118,8 @@ struct NOST : public ContractBase REGISTER_USER_FUNCTION(GetAuction, 1); REGISTER_USER_FUNCTION(GetAuctionParticipant, 2); REGISTER_USER_FUNCTION(GetTicksBeforeAuctionLaunch, 3); + REGISTER_USER_FUNCTION(GetAuctionFees, 4); + REGISTER_USER_FUNCTION(GetFeeRecipients, 5); } INITIALIZE() @@ -2637,6 +2690,35 @@ struct NOST : public ContractBase 0)); } + /** + * @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; + } + + /** + * @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; + } + /** * @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. From cb61cc23f357cd134f0e181067b120a046ad6ba2 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 16 Apr 2026 22:25:35 +0300 Subject: [PATCH 24/59] Refactor and streamline `ContractTestingNostromo`: remove redundant testing logic, consolidate helper functions, and introduce `ContractTestingNostromoAuctionFromScratch` class for auction-related test cases. --- src/contracts/Nostromo.h | 28 +- test/contract_nostromo.cpp | 3124 +++++++++++++++++------------------- 2 files changed, 1485 insertions(+), 1667 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index bb75d23d6..198df3365 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -33,6 +33,7 @@ constexpr uint64 NOST_SECONDS_PER_DAY = 86400ULL; constexpr uint64 NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS = 604800ULL; constexpr uint64 NOST_AUCTION_PRE_EPOCH_PAUSE_SECONDS = 1800ULL; constexpr uint32 NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS = 500U; +constexpr uint32 NOST_DEFAULT_INIT_TIME = 22 << 9 | 4 << 5 | 13; struct NOST2 { @@ -716,10 +717,17 @@ struct NOST : public ContractBase /** @brief Internal output of the auction interaction pause check. */ struct IsAuctionInteractionPaused_output { - /** @brief Flag indicating whether the 30-minute pre-epoch pause or 500-tick post-BEGIN_EPOCH pause is active. */ + /** @brief Flag indicating whether auction interactions are blocked by bootstrap time or by epoch timing pauses. */ uint8 isPaused; }; + /** @brief Internal locals used to evaluate whether auction interactions are currently paused. */ + struct IsAuctionInteractionPaused_locals + { + /** @brief Compact current date marker used to detect the bootstrap default time sentinel. */ + uint32 currentDateStamp; + }; + /** @brief Internal input used to split auction proceeds between seller and configured fee recipients. */ struct DistributeAuctionRevenue_input { @@ -1075,6 +1083,7 @@ struct NOST : public ContractBase AuctionData auction; DateAndTime currentDate; uint64 elapsedSeconds; + uint32 currentDateStamp; sint64 auctionIndex; GetTicksBeforeAuctionLaunchInternal_input getTicksBeforeAuctionLaunchInternalInput; GetTicksBeforeAuctionLaunchInternal_output getTicksBeforeAuctionLaunchInternalOutput; @@ -1189,6 +1198,12 @@ struct NOST : public ContractBase END_TICK_WITH_LOCALS() { + makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentDateStamp); + if (locals.currentDateStamp == NOST_DEFAULT_INIT_TIME) + { + return; + } + if (state.get().isPostBeginEpochPauseArmed) { CALL(GetTicksBeforeAuctionLaunchInternal, locals.getTicksBeforeAuctionLaunchInternalInput, @@ -1285,10 +1300,17 @@ struct NOST : public ContractBase output.isValid = output.lotItemCount > 0 ? 1 : 0; } - PRIVATE_FUNCTION(IsAuctionInteractionPaused) + PRIVATE_FUNCTION_WITH_LOCALS(IsAuctionInteractionPaused) { output.isPaused = 0; + makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentDateStamp); + if (locals.currentDateStamp == NOST_DEFAULT_INIT_TIME) + { + output.isPaused = 1; + return; + } + if (state.get().isPostBeginEpochPauseArmed && max(static_cast(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) - (static_cast(qpi.tick()) - static_cast(qpi.initialTick())), 0) > 0) @@ -2866,6 +2888,8 @@ struct NOST : public ContractBase static bool isZeroAsset(const Asset& asset) { return asset.assetName == 0 && isZero(asset.issuer); } + static void makeDateStamp(uint8 year, uint8 month, uint8 day, uint32& res) { res = static_cast(year << 9 | month << 5 | day); } + /** * @brief Compares two Nostromo timestamps. * @param a Left-hand date-time. diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index 6f9c8e67d..3c25b0842 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -1,1692 +1,1486 @@ #define NO_UEFI -#include -#include - #include "contract_testing.h" -static std::mt19937_64 rand64; +using namespace QPI; -static unsigned long long random(unsigned long long minValue, unsigned long long maxValue) +namespace { - if(minValue > maxValue) - { - return 0; - } - return minValue + rand64() % (maxValue - minValue); -} + static constexpr uint64 QX_ISSUE_ASSET_FEE = 1000000000ULL; + static const id NOST_CONTRACT_ID(NOST_CONTRACT_INDEX, 0, 0, 0); -static id getUser(unsigned long long i) -{ - return id(i, i / 2 + 4, i + 10, i * 3 + 8); -} + class ContractTestingNostromoAuctionFromScratch : protected ContractTesting + { + public: + ContractTestingNostromoAuctionFromScratch() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(NOST); + callSystemProcedure(NOST_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QX); + callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); + setNow(2026, 1, 1, 9, 0, 0); + } -static std::vector getRandomUsers(unsigned int totalUsers, unsigned int maxNum) -{ - 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; -} + void ensureUser(const id& user, sint64 amount = 1000) + { + if (getBalance(user) == 0) + { + increaseEnergy(user, amount); + } + } -class NostromoChecker : public NOST, public NOST::StateData -{ -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++) + 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.epoch; + callSystemProcedure(NOST_CONTRACT_INDEX, BEGIN_EPOCH); + } + + sint64 issueAsset(const id& issuer, uint64 assetName, sint64 numberOfShares) { - if (tmpInvestedList.get(i).indexOfFundraising == indexOfFundraising) + 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 + 1000); + invokeUserProcedure(QX_CONTRACT_INDEX, 1, input, output, issuer, QX_ISSUE_ASSET_FEE); + return output.issuedNumberOfShares; + } + + 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 = 0) + { + NOST::CreateAuction_output output{}; + if (reward > 0) + { + seedUser(seller, reward + 1000); + } + else + { + ensureUser(seller); + } + invokeUserProcedure(NOST_CONTRACT_INDEX, 1, input, output, seller, reward); + return output; + } + + NOST::PlaceBid_output placeBid(const id& bidder, const id& auctionId, uint64 quantity, uint64 bidAmount, sint64 reward) + { + NOST::PlaceBid_input input{}; + NOST::PlaceBid_output output{}; + + input.auctionId = auctionId; + input.quantity = quantity; + input.bidAmount = bidAmount; + + seedUser(bidder, reward + 1000); + invokeUserProcedure(NOST_CONTRACT_INDEX, 2, input, output, bidder, reward); + return output; + } + + NOST::CancelAuction_output cancelAuction(const id& seller, const id& auctionId, sint64 reward) + { + NOST::CancelAuction_input input{}; + NOST::CancelAuction_output output{}; + + input.auctionId = auctionId; + if (reward > 0) + { + seedUser(seller, reward + 1000); + } + else { - return tmpInvestedList.get(i).investedAmount; + ensureUser(seller); } + invokeUserProcedure(NOST_CONTRACT_INDEX, 3, input, output, seller, reward); + return output; } - 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) + NOST::TransferShareManagementRights_output transferManagedShares(const id& owner, const Asset& asset, sint64 numberOfShares, + uint32 contractIndex) { - userId = users.key(idx); - tierLevel = users.value(idx); + NOST::TransferShareManagementRights_input input{}; + NOST::TransferShareManagementRights_output output{}; - EXPECT_EQ(voteStatus.get(userId, votedList), 0); - EXPECT_EQ(numberOfVotedProject.get(userId, numberOfProject), 0); + input.asset = asset; + input.numberOfShares = numberOfShares; + input.newManagingContractIndex = contractIndex; - idx = users.nextElementIndex(idx); + ensureUser(owner); + invokeUserProcedure(NOST_CONTRACT_INDEX, 4, input, output, owner, 0); + return output; } - } - 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); - } -}; - -class ContractTestingNostromo : protected ContractTesting + + NOST::ResolvePendingStandardAuction_output resolvePendingStandardAuction(const id& seller, const id& auctionId, bool acceptSale) + { + NOST::ResolvePendingStandardAuction_input input{}; + NOST::ResolvePendingStandardAuction_output output{}; + + input.auctionId = auctionId; + 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::GetAuction_output getAuction(const id& auctionId) const + { + NOST::GetAuction_input input{}; + NOST::GetAuction_output output{}; + + input.auctionId = auctionId; + callFunction(NOST_CONTRACT_INDEX, 1, input, output); + return output; + } + + NOST::GetAuctionParticipant_output getParticipant(const id& auctionId, const id& participant) const + { + NOST::GetAuctionParticipant_input input{}; + NOST::GetAuctionParticipant_output output{}; + + input.auctionId = auctionId; + 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::GetFeeRecipients_output getFeeRecipients() const + { + NOST::GetFeeRecipients_input input{}; + NOST::GetFeeRecipients_output output{}; + + callFunction(NOST_CONTRACT_INDEX, 5, input, output); + return output; + } + + 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::AuctionLotEntry entry{}; + + entry.asset = asset; + entry.quantity = quantity; + lot.set(0, entry); + return lot; + } + + static Array makeTwoAssetLot(const Asset& assetA, sint64 quantityA, const Asset& assetB, + sint64 quantityB) + { + Array lot{}; + NOST::AuctionLotEntry entryA{}; + NOST::AuctionLotEntry entryB{}; + + entryA.asset = assetA; + entryA.quantity = quantityA; + entryB.asset = assetB; + entryB.quantity = quantityB; + lot.set(0, entryA); + lot.set(1, entryB); + 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.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 = 100, uint64 salePrice = 150, uint64 minimumBidIncrement = 10, + 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; + } + + 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); + } + }; + + uint64 expectedShareholderFeeBasisPoints(const NOST::GetAuctionFees_output& fees, uint64 grossAmount) + { + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1) + { + return fees.shareholderFeeBasisPointsTier1; + } + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2) + { + return fees.shareholderFeeBasisPointsTier2; + } + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3) + { + return fees.shareholderFeeBasisPointsTier3; + } + return fees.shareholderFeeBasisPointsTier4; + } + + uint64 expectedSellerPayout(const NOST::GetAuctionFees_output& fees, uint64 grossAmount) + { + const uint64 shareholderFeeAmount = grossAmount * expectedShareholderFeeBasisPoints(fees, grossAmount) / 10000ULL; + const uint64 managementFeeAmount = grossAmount * fees.managementFeeBasisPoints / 10000ULL; + const uint64 developmentFeeAmount = grossAmount * fees.developmentFeeBasisPoints / 10000ULL; + const uint64 takeoverCoordinatorBaseAmount = grossAmount * fees.takeoverCoordinatorFeeBasisPoints / 10000ULL; + return grossAmount - shareholderFeeAmount - managementFeeAmount - developmentFeeAmount - takeoverCoordinatorBaseAmount; + } + + uint64 expectedTakeoverCoordinatorGain(const NOST::GetAuctionFees_output& fees, uint64 grossAmount) + { + const uint64 shareholderFeeAmount = grossAmount * expectedShareholderFeeBasisPoints(fees, grossAmount) / 10000ULL; + const uint64 shareholderDividendAmount = shareholderFeeAmount * fees.shareholderDividendBasisPoints / 10000ULL; + const uint64 takeoverCoordinatorBaseAmount = grossAmount * fees.takeoverCoordinatorFeeBasisPoints / 10000ULL; + return takeoverCoordinatorBaseAmount + (shareholderFeeAmount - shareholderDividendAmount); + } + + uint64 expectedDividendRetention(const NOST::GetAuctionFees_output& fees, uint64 grossAmount) + { + const uint64 shareholderFeeAmount = grossAmount * expectedShareholderFeeBasisPoints(fees, grossAmount) / 10000ULL; + return shareholderFeeAmount * fees.shareholderDividendBasisPoints / 10000ULL; + } +} // namespace + +TEST(ContractNostromoAuction, InitialStateAndGettersAuction) +{ + ContractTestingNostromoAuctionFromScratch nostromo; + + const auto fees = nostromo.getAuctionFees(); + EXPECT_EQ(fees.privateAuctionFee, NOST_DEFAULT_PRIVATE_AUCTION_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, ContractTestingNostromoAuctionFromScratch::managementWallet()); + EXPECT_EQ(recipients.development, ContractTestingNostromoAuctionFromScratch::developmentWallet()); + EXPECT_EQ(recipients.takeoverCoordinator, ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet()); + + const id missingAuction(777, 0, 0, 0); + 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_TRUE(isZero(auctionOutput.auction.auctionId)); + EXPECT_EQ(participantOutput.found, 0); + EXPECT_EQ(launchPause.ticks, 0U); + + nostromo.beginEpoch(); + 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, TransferShareManagementRightsAuction) +{ + ContractTestingNostromoAuctionFromScratch 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); + + Asset zeroAsset{}; + const auto invalidZeroAsset = nostromo.transferManagedShares(owner, zeroAsset, 1, QX_CONTRACT_INDEX); + EXPECT_EQ(invalidZeroAsset.transferredNumberOfShares, 0); + + const auto invalidZeroContract = nostromo.transferManagedShares(owner, asset, 1, 0); + EXPECT_EQ(invalidZeroContract.transferredNumberOfShares, 0); + + const auto insufficient = nostromo.transferManagedShares(owner, asset, 8, QX_CONTRACT_INDEX); + EXPECT_EQ(insufficient.transferredNumberOfShares, 0); + + const auto success = nostromo.transferManagedShares(owner, asset, 5, QX_CONTRACT_INDEX); + EXPECT_EQ(success.transferredNumberOfShares, 5); + EXPECT_EQ(nostromo.managedShares(asset, owner), 2); + EXPECT_EQ(nostromo.sharesManagedBy(asset, owner, QX_CONTRACT_INDEX), 8); +} + +TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) +{ + ContractTestingNostromoAuctionFromScratch 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 = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 9, 25); + const auto output = nostromo.createAuction(seller, input); + ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_FALSE(isZero(output.auctionId)); + + const auto auction = nostromo.getAuction(output.auctionId).auction; + EXPECT_EQ(auction.auctionId, output.auctionId); + EXPECT_EQ(auction.quantityForSale, 9ULL); + EXPECT_EQ(auction.minimumPurchaseQuantity, 0ULL); + EXPECT_EQ(auction.salePrice, 25ULL); + EXPECT_EQ(auction.auctionDurationSeconds, NOST_SECONDS_PER_DAY); + EXPECT_EQ(auction.seller, seller); + EXPECT_EQ(auction.type, NOST::EAuctionType::Batch); + EXPECT_EQ(auction.visibility, NOST::EAuctionVisibility::Public); + EXPECT_EQ(auction.status, NOST::EAuctionStatus::Active); + EXPECT_EQ(auction.auctionLotItems.get(0).asset, asset); + EXPECT_EQ(auction.auctionLotItems.get(0).quantity, 9); + EXPECT_EQ(auction.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, CreateStandardBundleAuctionEscrowsMixedLotAuction) +{ + ContractTestingNostromoAuctionFromScratch nostromo; + const id seller(21, 22, 23, 24); + const uint64 assetNameA = assetNameFromString("CRTSTA"); + const uint64 assetNameB = assetNameFromString("CRTSTB"); + const Asset assetA{seller, assetNameA}; + const Asset assetB{seller, assetNameB}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetNameA, 2), 2); + EXPECT_EQ(nostromo.issueAsset(seller, assetNameB, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetA, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetB, 3), 3); + + auto input = ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( + ContractTestingNostromoAuctionFromScratch::makeTwoAssetLot(assetA, 2, assetB, 3), 100, 150, 5); + + const auto output = nostromo.createAuction(seller, input); + ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); + + const auto auction = nostromo.getAuction(output.auctionId).auction; + EXPECT_EQ(auction.quantityForSale, 1ULL); + EXPECT_EQ(auction.minimumPurchaseQuantity, 1ULL); + EXPECT_EQ(auction.initialPrice, 100ULL); + EXPECT_EQ(auction.salePrice, 150ULL); + EXPECT_EQ(auction.minimumBidIncrement, 5ULL); + EXPECT_EQ(auction.type, NOST::EAuctionType::Standard); + EXPECT_EQ(auction.auctionLotItems.get(0).asset, assetA); + EXPECT_EQ(auction.auctionLotItems.get(0).quantity, 2); + EXPECT_EQ(auction.auctionLotItems.get(1).asset, assetB); + EXPECT_EQ(auction.auctionLotItems.get(1).quantity, 3); + EXPECT_EQ(nostromo.sharesManagedBy(assetA, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 2); + EXPECT_EQ(nostromo.sharesManagedBy(assetB, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 3); +} + +TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction) +{ + { + ContractTestingNostromoAuctionFromScratch 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 = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 4, 12); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNostromoAuctionFromScratch::makeAllowedWallets({allowedBidder}); + + const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); + + const auto auction = nostromo.getAuction(output.auctionId).auction; + EXPECT_EQ(auction.visibility, NOST::EAuctionVisibility::Private); + EXPECT_TRUE(auction.allowedBidderWallets.contains(allowedBidder)); + EXPECT_EQ(auction.requiredAccessAssets.population(), 0U); + } + + { + ContractTestingNostromoAuctionFromScratch 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 = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(saleAsset, 5, 20); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.requiredAccessAssets = ContractTestingNostromoAuctionFromScratch::makeRequiredAccessAssets({gateAsset}); + + const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); + + const auto auction = nostromo.getAuction(output.auctionId).auction; + EXPECT_EQ(auction.visibility, NOST::EAuctionVisibility::Private); + EXPECT_EQ(auction.allowedBidderWallets.population(), 0U); + EXPECT_TRUE(auction.requiredAccessAssets.contains(gateAsset)); + EXPECT_GT(nostromo.plainShares(gateAsset, gatedBidder), 0); + } +} + +TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) +{ + ContractTestingNostromoAuctionFromScratch nostromo; + const id seller(51, 52, 53, 54); + const id altIssuer(55, 56, 57, 58); + const uint64 assetNameA = assetNameFromString("INVAAA"); + const uint64 assetNameB = assetNameFromString("INVBBB"); + const Asset assetA{seller, assetNameA}; + const Asset assetB{seller, assetNameB}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetNameA, 5), 5); + EXPECT_EQ(nostromo.issueAsset(seller, assetNameB, 5), 5); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetA, 5), 5); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetB, 5), 5); + EXPECT_EQ(nostromo.issueAsset(altIssuer, assetNameFromString("GATINV"), 1), 1); + + auto invalidCid = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + invalidCid.metadataIpfsCid = ContractTestingNostromoAuctionFromScratch::makeInvalidMetadataCidFirstChar(); + EXPECT_EQ(nostromo.createAuction(seller, invalidCid).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + auto invalidCidUppercase = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + invalidCidUppercase.metadataIpfsCid = ContractTestingNostromoAuctionFromScratch::makeInvalidMetadataCidUppercase(); + EXPECT_EQ(nostromo.createAuction(seller, invalidCidUppercase).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + auto emptyLot = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + emptyLot.auctionLotItems = Array{}; + EXPECT_EQ(nostromo.createAuction(seller, emptyLot).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + auto negativeQuantity = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + negativeQuantity.auctionLotItems = ContractTestingNostromoAuctionFromScratch::makeSingleLot(assetA, -1); + EXPECT_EQ(nostromo.createAuction(seller, negativeQuantity).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + auto zeroDuration = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + zeroDuration.durationDays = 0; + EXPECT_EQ(nostromo.createAuction(seller, zeroDuration).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + auto tooLongDuration = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + tooLongDuration.durationDays = NOST_AUCTION_MAX_DURATION_DAYS + 1; + EXPECT_EQ(nostromo.createAuction(seller, tooLongDuration).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + auto invalidType = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + invalidType.auctionType = 99; + EXPECT_EQ(nostromo.createAuction(seller, invalidType).errorCode, static_cast(NOST::EAuctionError::InvalidAuctionType)); + + auto invalidVisibility = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + invalidVisibility.auctionVisibility = 99; + EXPECT_EQ(nostromo.createAuction(seller, invalidVisibility).errorCode, static_cast(NOST::EAuctionError::InvalidVisibility)); + + auto invalidBatchBundle = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + invalidBatchBundle.auctionLotItems = ContractTestingNostromoAuctionFromScratch::makeTwoAssetLot(assetA, 2, assetB, 3); + EXPECT_EQ(nostromo.createAuction(seller, invalidBatchBundle).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + auto invalidBatchBuyNow = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + invalidBatchBuyNow.buyNowPrice = 100; + EXPECT_EQ(nostromo.createAuction(seller, invalidBatchBuyNow).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + auto invalidStandardMinimumPurchase = + ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput(ContractTestingNostromoAuctionFromScratch::makeSingleLot(assetA, 1)); + invalidStandardMinimumPurchase.minimumPurchaseQuantity = 2; + EXPECT_EQ(nostromo.createAuction(seller, invalidStandardMinimumPurchase).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + auto invalidStandardIncrement = + ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput(ContractTestingNostromoAuctionFromScratch::makeSingleLot(assetA, 1)); + invalidStandardIncrement.minimumBidIncrement = 0; + EXPECT_EQ(nostromo.createAuction(seller, invalidStandardIncrement).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + auto invalidStandardPrice = ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( + ContractTestingNostromoAuctionFromScratch::makeSingleLot(assetA, 1), 200, 150, 10); + EXPECT_EQ(nostromo.createAuction(seller, invalidStandardPrice).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + auto invalidStandardSalePrice = + ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput(ContractTestingNostromoAuctionFromScratch::makeSingleLot(assetA, 1)); + invalidStandardSalePrice.salePrice = 0; + EXPECT_EQ(nostromo.createAuction(seller, invalidStandardSalePrice).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + auto invalidStandardBuyNow = ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( + ContractTestingNostromoAuctionFromScratch::makeSingleLot(assetA, 1), 100, 150, 10, 140); + EXPECT_EQ(nostromo.createAuction(seller, invalidStandardBuyNow).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + auto privateWithoutGate = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + privateWithoutGate.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + EXPECT_EQ(nostromo.createAuction(seller, privateWithoutGate, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, + static_cast(NOST::EAuctionError::InvalidInput)); + + auto privateWithBothGates = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + privateWithBothGates.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + privateWithBothGates.allowedBidderWallets = ContractTestingNostromoAuctionFromScratch::makeAllowedWallets({id(99, 1, 1, 1)}); + privateWithBothGates.requiredAccessAssets = + ContractTestingNostromoAuctionFromScratch::makeRequiredAccessAssets({Asset{altIssuer, assetNameFromString("GATINV")}}); + EXPECT_EQ(nostromo.createAuction(seller, privateWithBothGates, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, + static_cast(NOST::EAuctionError::InvalidInput)); +} + +TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientAssetBalanceAndPauseAuction) +{ + { + ContractTestingNostromoAuctionFromScratch 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 = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 4, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNostromoAuctionFromScratch::makeAllowedWallets({id(1, 1, 1, 1)}); + + const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE - 1); + EXPECT_EQ(output.errorCode, static_cast(NOST::EAuctionError::InsufficientFunds)); + EXPECT_EQ(nostromo.managedShares(asset, seller), 4); + } + + { + ContractTestingNostromoAuctionFromScratch 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 = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 3, 10); + const auto output = nostromo.createAuction(seller, input); + EXPECT_EQ(output.errorCode, static_cast(NOST::EAuctionError::InsufficientAssetBalance)); + EXPECT_EQ(nostromo.managedShares(asset, seller), 2); + } + + { + ContractTestingNostromoAuctionFromScratch 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 = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 3, 10); + nostromo.setNow(2026, 1, 7, 11, 40, 0); + const auto output = nostromo.createAuction(seller, input); + EXPECT_EQ(output.errorCode, static_cast(NOST::EAuctionError::AuctionPaused)); + EXPECT_TRUE(isZero(output.auctionId)); + EXPECT_EQ(nostromo.managedShares(asset, seller), 3); + } + + { + ContractTestingNostromoAuctionFromScratch 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 = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 3, 10); + nostromo.setNow(2022, 4, 13, 12, 0, 0); + const auto output = nostromo.createAuction(seller, input); + EXPECT_EQ(output.errorCode, static_cast(NOST::EAuctionError::AuctionPaused)); + EXPECT_TRUE(isZero(output.auctionId)); + EXPECT_EQ(nostromo.managedShares(asset, seller), 3); + } +} + +TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAuction) +{ + ContractTestingNostromoAuctionFromScratch 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, ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 6, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + + const auto sellerBid = nostromo.placeBid(seller, createOutput.auctionId, 1, 12, 12); + EXPECT_EQ(sellerBid.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + + const auto missingAuction = nostromo.placeBid(bidderA, id(700, 0, 0, 0), 1, 12, 12); + EXPECT_EQ(missingAuction.errorCode, static_cast(NOST::EAuctionError::AuctionNotFound)); + + const auto zeroQuantity = nostromo.placeBid(bidderA, createOutput.auctionId, 0, 12, 12); + EXPECT_EQ(zeroQuantity.errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + const auto zeroBid = nostromo.placeBid(bidderA, createOutput.auctionId, 1, 0, 1); + EXPECT_EQ(zeroBid.errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + const auto tooLow = nostromo.placeBid(bidderA, createOutput.auctionId, 1, 9, 9); + EXPECT_EQ(tooLow.errorCode, static_cast(NOST::EAuctionError::BidTooLow)); + + const auto insufficientFunds = nostromo.placeBid(bidderA, createOutput.auctionId, 2, 12, 23); + EXPECT_EQ(insufficientFunds.errorCode, static_cast(NOST::EAuctionError::InsufficientFunds)); + + const auto bidA1 = nostromo.placeBid(bidderA, createOutput.auctionId, 2, 20, 40); + const auto bidB = nostromo.placeBid(bidderB, createOutput.auctionId, 3, 15, 45); + ASSERT_EQ(bidA1.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(bidB.errorCode, static_cast(NOST::EAuctionError::Success)); + + auto auction = nostromo.getAuction(createOutput.auctionId).auction; + EXPECT_EQ(auction.highestBidder, bidderA); + EXPECT_EQ(auction.highestBidPrice, 20ULL); + EXPECT_EQ(auction.highestBidAmount, 40ULL); + + const auto bidA2 = nostromo.placeBid(bidderA, createOutput.auctionId, 2, 14, 28); + EXPECT_EQ(bidA2.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(bidA2.escrowedAmount, 28ULL); + EXPECT_EQ(bidA2.refundedAmount, 40ULL); + + auction = nostromo.getAuction(createOutput.auctionId).auction; + EXPECT_EQ(auction.highestBidder, bidderB); + EXPECT_EQ(auction.highestBidPrice, 15ULL); + EXPECT_EQ(auction.highestBidAmount, 45ULL); + + const auto participantA = nostromo.getParticipant(createOutput.auctionId, bidderA); + ASSERT_EQ(participantA.found, 1); + EXPECT_EQ(participantA.participantData.escrowedAmount, 28ULL); + EXPECT_EQ(participantA.participantData.bidAmount, 14ULL); + + nostromo.setNow(2026, 1, 2, 9, 0, 1); + const auto closed = nostromo.placeBid(bidderC, createOutput.auctionId, 1, 30, 30); + EXPECT_EQ(closed.errorCode, static_cast(NOST::EAuctionError::AuctionClosed)); +} + +TEST(ContractNostromoAuction, PlaceBatchBidExtendsAuctionNearEndAuction) +{ + ContractTestingNostromoAuctionFromScratch 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, ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 2, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + + nostromo.setNow(2026, 1, 2, 8, 56, 30); + const auto bidOutput = nostromo.placeBid(bidder, createOutput.auctionId, 1, 15, 15); + ASSERT_EQ(bidOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + EXPECT_EQ(auction.auctionDurationSeconds, NOST_SECONDS_PER_DAY + NOST_AUCTION_EXTENSION_SECONDS); +} + +TEST(ContractNostromoAuction, PlaceStandardBidValidatesRefundsAndPauseAuction) +{ + ContractTestingNostromoAuctionFromScratch 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, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( + ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + + const auto sellerBid = nostromo.placeBid(seller, createOutput.auctionId, 1, 100, 100); + EXPECT_EQ(sellerBid.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + + const auto lowStart = nostromo.placeBid(bidderA, createOutput.auctionId, 1, 99, 99); + EXPECT_EQ(lowStart.errorCode, static_cast(NOST::EAuctionError::BidTooLow)); + + const auto openingBid = nostromo.placeBid(bidderA, createOutput.auctionId, 1, 100, 100); + ASSERT_EQ(openingBid.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(openingBid.escrowedAmount, 100ULL); + + const auto lowIncrement = nostromo.placeBid(bidderB, createOutput.auctionId, 1, 109, 109); + EXPECT_EQ(lowIncrement.errorCode, static_cast(NOST::EAuctionError::BidTooLow)); + + const auto outbid = nostromo.placeBid(bidderB, createOutput.auctionId, 1, 110, 110); + ASSERT_EQ(outbid.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(outbid.refundedAmount, 100ULL); + + const auto bidderAState = nostromo.getParticipant(createOutput.auctionId, bidderA); + const auto bidderBState = nostromo.getParticipant(createOutput.auctionId, 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, 110ULL); + EXPECT_EQ(bidderBState.participantData.isWinningBid, 1u); + + const auto bidderBImprove = nostromo.placeBid(bidderB, createOutput.auctionId, 1, 130, 130); + EXPECT_EQ(bidderBImprove.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(bidderBImprove.refundedAmount, 110ULL); + EXPECT_EQ(bidderBImprove.escrowedAmount, 130ULL); + + 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.auctionId, 1, 140, 140); + EXPECT_EQ(pausedBid.errorCode, static_cast(NOST::EAuctionError::AuctionPaused)); + nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + + const auto resumedBid = nostromo.placeBid(id(137, 138, 139, 140), createOutput.auctionId, 1, 140, 140); + EXPECT_EQ(resumedBid.errorCode, static_cast(NOST::EAuctionError::Success)); + + nostromo.setNow(2022, 4, 13, 12, 0, 0); + const auto bootstrapPausedBid = nostromo.placeBid(id(141, 142, 143, 144), createOutput.auctionId, 1, 150, 150); + EXPECT_EQ(bootstrapPausedBid.errorCode, static_cast(NOST::EAuctionError::AuctionPaused)); +} + +TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) { -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; - } -}; - -TEST(TestContractNostromo, registerAndLogoutAndUpgradeFromTierChecker) + { + ContractTestingNostromoAuctionFromScratch 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 = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 3, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.allowedBidderWallets = ContractTestingNostromoAuctionFromScratch::makeAllowedWallets({allowed}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + + EXPECT_EQ(nostromo.placeBid(denied, createOutput.auctionId, 1, 12, 12).errorCode, + static_cast(NOST::EAuctionError::PrivateAuctionAccessDenied)); + EXPECT_EQ(nostromo.placeBid(allowed, createOutput.auctionId, 1, 12, 12).errorCode, static_cast(NOST::EAuctionError::Success)); + } + + { + ContractTestingNostromoAuctionFromScratch nostromo; + const id seller(153, 154, 155, 156); + const id allowed(157, 158, 159, 160); + const id denied(161, 162, 163, 164); + const uint64 saleAssetName = assetNameFromString("PRIACS"); + const uint64 accessAssetName = assetNameFromString("PRIACG"); + const Asset saleAsset{seller, saleAssetName}; + const Asset accessAsset{allowed, accessAssetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, saleAssetName, 3), 3); + EXPECT_EQ(nostromo.issueAsset(allowed, accessAssetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, saleAsset, 3), 3); + + auto input = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(saleAsset, 3, 10); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); + input.requiredAccessAssets = ContractTestingNostromoAuctionFromScratch::makeRequiredAccessAssets({accessAsset}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + + EXPECT_EQ(nostromo.placeBid(denied, createOutput.auctionId, 1, 12, 12).errorCode, + static_cast(NOST::EAuctionError::PrivateAuctionAccessDenied)); + EXPECT_EQ(nostromo.placeBid(allowed, createOutput.auctionId, 1, 12, 12).errorCode, static_cast(NOST::EAuctionError::Success)); + } +} + +TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) { - 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); + ContractTestingNostromoAuctionFromScratch nostromo; + const id seller(171, 172, 173, 174); + const id bidder(175, 176, 177, 178); + const uint64 assetNameA = assetNameFromString("BUYNWA"); + const uint64 assetNameB = assetNameFromString("BUYNWB"); + const Asset assetA{seller, assetNameA}; + const Asset assetB{seller, assetNameB}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetNameA, 2), 2); + EXPECT_EQ(nostromo.issueAsset(seller, assetNameB, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetA, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetB, 1), 1); + + auto input = ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( + ContractTestingNostromoAuctionFromScratch::makeTwoAssetLot(assetA, 2, assetB, 1), 100, 150, 10, 180); + const sint64 sellerBalanceBefore = getBalance(seller); + const auto fees = nostromo.getAuctionFees(); + const auto createOutput = nostromo.createAuction(seller, input); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + + const auto bidOutput = nostromo.placeBid(bidder, createOutput.auctionId, 1, 180, 180); + ASSERT_EQ(bidOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto participant = nostromo.getParticipant(createOutput.auctionId, bidder); + EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.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(assetA, bidder), 2); + EXPECT_EQ(nostromo.managedShares(assetB, bidder), 1); + EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, expectedSellerPayout(fees, 180ULL)); +} + +TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialFillAuction) +{ + { + ContractTestingNostromoAuctionFromScratch 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, ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 4, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + + ASSERT_EQ(nostromo.placeBid(bidderA, createOutput.auctionId, 3, 15, 45).errorCode, static_cast(NOST::EAuctionError::Success)); + nostromo.setNow(2026, 1, 1, 9, 0, 1); + ASSERT_EQ(nostromo.placeBid(bidderB, createOutput.auctionId, 3, 15, 45).errorCode, static_cast(NOST::EAuctionError::Success)); + nostromo.setNow(2026, 1, 1, 9, 0, 2); + ASSERT_EQ(nostromo.placeBid(bidderC, createOutput.auctionId, 2, 20, 40).errorCode, static_cast(NOST::EAuctionError::Success)); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto participantA = nostromo.getParticipant(createOutput.auctionId, bidderA); + const auto participantB = nostromo.getParticipant(createOutput.auctionId, bidderB); + const auto participantC = nostromo.getParticipant(createOutput.auctionId, bidderC); + + EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.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); + } + + { + ContractTestingNostromoAuctionFromScratch 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, ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 5, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 2, 12, 24).errorCode, static_cast(NOST::EAuctionError::Success)); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.allocatedQuantity, 2ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 2); + EXPECT_EQ(nostromo.managedShares(asset, seller), 3); + } +} + +TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) +{ + ContractTestingNostromoAuctionFromScratch 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, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( + ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.allocatedQuantity, 0ULL); + EXPECT_TRUE(isZero(auction.highestBidder)); + EXPECT_EQ(nostromo.managedShares(asset, seller), 1); +} + +TEST(ContractNostromoAuction, EndTickSkipsAuctionProcessingAtBootstrapTimeAuction) +{ + ContractTestingNostromoAuctionFromScratch 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, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( + ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + + nostromo.setNow(2022, 4, 13, 12, 0, 0); + nostromo.advanceAndEndTick(0); + + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + EXPECT_EQ(auction.status, NOST::EAuctionStatus::Active); + EXPECT_EQ(auction.allocatedQuantity, 0ULL); + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); +} + +TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) +{ + ContractTestingNostromoAuctionFromScratch nostromo; + const id seller(221, 222, 223, 224); + const id bidder(225, 226, 227, 228); + const uint64 assetName = assetNameFromString("STDEND"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto fees = nostromo.getAuctionFees(); + const sint64 sellerBalanceBefore = getBalance(seller); + const sint64 managementBefore = getBalance(ContractTestingNostromoAuctionFromScratch::managementWallet()); + const sint64 developmentBefore = getBalance(ContractTestingNostromoAuctionFromScratch::developmentWallet()); + const sint64 coordinatorBefore = getBalance(ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( + ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 10000, 10000, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 10000, 10000).errorCode, static_cast(NOST::EAuctionError::Success)); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.allocatedQuantity, 1ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); + EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, expectedSellerPayout(fees, 10000ULL)); + EXPECT_EQ(getBalance(ContractTestingNostromoAuctionFromScratch::managementWallet()) - managementBefore, 50); + EXPECT_EQ(getBalance(ContractTestingNostromoAuctionFromScratch::developmentWallet()) - developmentBefore, 50); + EXPECT_EQ(getBalance(ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet()) - coordinatorBefore, + expectedTakeoverCoordinatorGain(fees, 10000ULL)); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendRetention(fees, 10000ULL)); +} + +TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction) +{ + { + ContractTestingNostromoAuctionFromScratch 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, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( + ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::PendingSellerDecision); + + const auto forbidden = nostromo.resolvePendingStandardAuction(id(999, 999, 999, 999), createOutput.auctionId, true); + EXPECT_EQ(forbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + + const auto acceptOutput = nostromo.resolvePendingStandardAuction(seller, createOutput.auctionId, true); + EXPECT_EQ(acceptOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); + } + + { + ContractTestingNostromoAuctionFromScratch 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, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( + ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + const auto rejectOutput = nostromo.resolvePendingStandardAuction(seller, createOutput.auctionId, false); + EXPECT_EQ(rejectOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(rejectOutput.refundedAmount, 120ULL); + + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto participant = nostromo.getParticipant(createOutput.auctionId, bidder); + EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.allocatedQuantity, 0ULL); + EXPECT_TRUE(isZero(auction.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); + } + + { + ContractTestingNostromoAuctionFromScratch 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, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( + ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::PendingSellerDecision); + + nostromo.advanceAndEndTick((NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS + 1ULL) * 1000ULL); + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto participant = nostromo.getParticipant(createOutput.auctionId, bidder); + EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.allocatedQuantity, 1ULL); + ASSERT_EQ(participant.found, 1); + EXPECT_EQ(participant.participantData.allocatedQuantity, 1ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); + } +} + +TEST(ContractNostromoAuction, CancelAuctionRefundsAndChargesCorrectFeeAuction) +{ + { + ContractTestingNostromoAuctionFromScratch nostromo; + const id seller(261, 262, 263, 264); + const id bidderA(265, 266, 267, 268); + const id bidderB(269, 270, 271, 272); + const uint64 assetName = assetNameFromString("CANBAT"); + const Asset asset{seller, assetName}; + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 10), 10); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 10, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidderA, createOutput.auctionId, 2, 20, 40).errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidderB, createOutput.auctionId, 3, 15, 45).errorCode, static_cast(NOST::EAuctionError::Success)); + + const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionId, 10); + EXPECT_EQ(cancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(cancelOutput.refundedAmount, 85ULL); + EXPECT_EQ(cancelOutput.cancellationFee, 10ULL); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Cancelled); + EXPECT_EQ(nostromo.managedShares(asset, seller), 10); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 10); + } + + { + ContractTestingNostromoAuctionFromScratch nostromo; + const id seller(273, 274, 275, 276); + const id bidder(277, 278, 279, 280); + const uint64 assetName = assetNameFromString("CANSTD"); + const Asset asset{seller, assetName}; + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( + ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); + + const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionId, 15); + EXPECT_EQ(cancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(cancelOutput.refundedAmount, 120ULL); + EXPECT_EQ(cancelOutput.cancellationFee, 15ULL); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Cancelled); + EXPECT_EQ(nostromo.managedShares(asset, seller), 1); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 15); + } +} + +TEST(ContractNostromoAuction, CancelAuctionRejectsInvalidCasesAuction) +{ + ContractTestingNostromoAuctionFromScratch 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, ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 2, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 12, 12).errorCode, static_cast(NOST::EAuctionError::Success)); + + const auto notFound = nostromo.cancelAuction(seller, id(800, 0, 0, 0), 10); + EXPECT_EQ(notFound.errorCode, static_cast(NOST::EAuctionError::AuctionNotFound)); + + const auto forbidden = nostromo.cancelAuction(bidder, createOutput.auctionId, 10); + EXPECT_EQ(forbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + + const auto insufficient = nostromo.cancelAuction(seller, createOutput.auctionId, 1); + EXPECT_EQ(insufficient.errorCode, static_cast(NOST::EAuctionError::InsufficientFunds)); + + const auto success = nostromo.cancelAuction(seller, createOutput.auctionId, 2); + EXPECT_EQ(success.errorCode, static_cast(NOST::EAuctionError::Success)); + + const auto closed = nostromo.cancelAuction(seller, createOutput.auctionId, 2); + EXPECT_EQ(closed.errorCode, static_cast(NOST::EAuctionError::AuctionClosed)); } -TEST(TestContractNostromo, createProjectAndVoteInProjectChecker) +TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) { - 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); + ContractTestingNostromoAuctionFromScratch nostromo; + const id outsider(291, 292, 293, 294); + const id newManagement(295, 296, 297, 298); + + NOST::SetAuctionFees_input coordinatorInput{}; + coordinatorInput.privateAuctionFee = 60000000; + 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 coordinatorForbidden = nostromo.setAuctionFees(outsider, coordinatorInput); + EXPECT_EQ(coordinatorForbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + + NOST::SetAuctionFees_input invalidCoordinatorInput = coordinatorInput; + invalidCoordinatorInput.privateAuctionFee = -1; + const auto coordinatorInvalid = + nostromo.setAuctionFees(ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet(), invalidCoordinatorInput); + EXPECT_EQ(coordinatorInvalid.errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + const auto coordinatorSuccess = nostromo.setAuctionFees(ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet(), coordinatorInput); + EXPECT_EQ(coordinatorSuccess.errorCode, static_cast(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); + + const auto setManagementForbidden = nostromo.setManagement(outsider, newManagement); + EXPECT_EQ(setManagementForbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + + const auto setManagementInvalid = nostromo.setManagement(ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet(), NULL_ID); + EXPECT_EQ(setManagementInvalid.errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + const auto setManagementSuccess = nostromo.setManagement(ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet(), newManagement); + EXPECT_EQ(setManagementSuccess.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(nostromo.getFeeRecipients().management, newManagement); + + NOST::SetAuctionFeesByManagement_input managementInput{}; + managementInput.privateAuctionFee = 70000000; + managementInput.auctionCancellationFeeBasisPoints = 800; + managementInput.managementFeeBasisPoints = 90; + managementInput.developmentFeeBasisPoints = 110; + managementInput.shareholderFeeBasisPointsTier1 = 300; + managementInput.shareholderFeeBasisPointsTier2 = 250; + managementInput.shareholderFeeBasisPointsTier3 = 200; + managementInput.shareholderFeeBasisPointsTier4 = 150; + + const auto oldManagementForbidden = + nostromo.setAuctionFeesByManagement(ContractTestingNostromoAuctionFromScratch::managementWallet(), managementInput); + EXPECT_EQ(oldManagementForbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + + NOST::SetAuctionFeesByManagement_input invalidManagementInput = managementInput; + invalidManagementInput.managementFeeBasisPoints = 9900; + invalidManagementInput.developmentFeeBasisPoints = 200; + const auto managementInvalid = nostromo.setAuctionFeesByManagement(newManagement, invalidManagementInput); + EXPECT_EQ(managementInvalid.errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + + const auto managementSuccess = nostromo.setAuctionFeesByManagement(newManagement, managementInput); + EXPECT_EQ(managementSuccess.errorCode, static_cast(NOST::EAuctionError::Success)); + + fees = nostromo.getAuctionFees(); + EXPECT_EQ(fees.privateAuctionFee, 70000000); + 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(TestContractNostromo, createFundraisingAndInvestInProjectAndClaimTokenChecker) +TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) { - 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(); + struct TierCase + { + uint64 grossAmount; + uint64 expectedShareholderFeeBp; + uint64 assetName; + }; + + const TierCase cases[] = { + {5000000000ULL, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_1, assetNameFromString("TIERA1")}, + {5000000001ULL, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_2, assetNameFromString("TIERA2")}, + {50000000001ULL, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_3, assetNameFromString("TIERA3")}, + {200000000001ULL, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4, assetNameFromString("TIERA4")}, + }; + + for (uint64 caseIndex = 0; caseIndex < sizeof(cases) / sizeof(cases[0]); ++caseIndex) + { + ContractTestingNostromoAuctionFromScratch nostromo; + const id seller(301 + caseIndex, 302 + caseIndex, 303 + caseIndex, 304 + caseIndex); + const id bidder(401 + caseIndex, 402 + caseIndex, 403 + caseIndex, 404 + caseIndex); + const Asset asset{seller, cases[caseIndex].assetName}; + const auto fees = nostromo.getAuctionFees(); + const sint64 sellerBefore = getBalance(seller); + const sint64 managementBefore = getBalance(ContractTestingNostromoAuctionFromScratch::managementWallet()); + const sint64 developmentBefore = getBalance(ContractTestingNostromoAuctionFromScratch::developmentWallet()); + const sint64 coordinatorBefore = getBalance(ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + + EXPECT_EQ(nostromo.issueAsset(seller, cases[caseIndex].assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + + const auto createOutput = nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( + ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), + cases[caseIndex].grossAmount, cases[caseIndex].grossAmount, 1)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, cases[caseIndex].grossAmount, cases[caseIndex].grossAmount).errorCode, + static_cast(NOST::EAuctionError::Success)); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + EXPECT_EQ(expectedShareholderFeeBasisPoints(fees, cases[caseIndex].grossAmount), cases[caseIndex].expectedShareholderFeeBp); + EXPECT_EQ(getBalance(seller) - sellerBefore, expectedSellerPayout(fees, cases[caseIndex].grossAmount)); + EXPECT_EQ(getBalance(ContractTestingNostromoAuctionFromScratch::managementWallet()) - managementBefore, + cases[caseIndex].grossAmount * fees.managementFeeBasisPoints / 10000ULL); + EXPECT_EQ(getBalance(ContractTestingNostromoAuctionFromScratch::developmentWallet()) - developmentBefore, + cases[caseIndex].grossAmount * fees.developmentFeeBasisPoints / 10000ULL); + EXPECT_EQ(getBalance(ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet()) - coordinatorBefore, + expectedTakeoverCoordinatorGain(fees, cases[caseIndex].grossAmount)); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendRetention(fees, cases[caseIndex].grossAmount)); + } } From bb4c0f2b14cdfb9aa809c53977fdacd925c51fc6 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 16 Apr 2026 23:29:04 +0300 Subject: [PATCH 25/59] Add global auction pause mechanism in `Nostromo`: implement auction timer pause intervals, synchronize deadlines with pause states, and add test cases for deadline adjustments. --- src/contracts/Nostromo.h | 214 ++++++++++++++++++++++++++++++++----- test/contract_nostromo.cpp | 79 ++++++++++++++ 2 files changed, 265 insertions(+), 28 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 198df3365..1ed32e263 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -251,12 +251,21 @@ struct NOST : public ContractBase /** @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 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; + id management; id development; @@ -721,13 +730,49 @@ struct NOST : public ContractBase uint8 isPaused; }; - /** @brief Internal locals used to evaluate whether auction interactions are currently paused. */ - struct IsAuctionInteractionPaused_locals + /** @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; + DateAndTime pauseStartedAt; + DateAndTime pauseEndsAt; + uint64 elapsedTicksSinceInitialTick; 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 { @@ -757,6 +802,20 @@ struct NOST : public ContractBase 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 process a batch auction bid after the common PlaceBid checks succeed. */ struct ProcessBatchBid_input { @@ -1072,6 +1131,8 @@ struct NOST : public ContractBase { AuctionData auction; DateAndTime currentDate; + IsAuctionInteractionPaused_input isAuctionInteractionPausedInput; + IsAuctionInteractionPaused_output isAuctionInteractionPausedOutput; FinalizeStandardAuction_input finalizeStandardAuctionInput; FinalizeStandardAuction_output finalizeStandardAuctionOutput; RejectStandardAuction_input rejectStandardAuctionInput; @@ -1082,11 +1143,11 @@ struct NOST : public ContractBase { AuctionData auction; DateAndTime currentDate; + SyncAuctionPauseState_input syncAuctionPauseStateInput; + SyncAuctionPauseState_output syncAuctionPauseStateOutput; uint64 elapsedSeconds; uint32 currentDateStamp; sint64 auctionIndex; - GetTicksBeforeAuctionLaunchInternal_input getTicksBeforeAuctionLaunchInternalInput; - GetTicksBeforeAuctionLaunchInternal_output getTicksBeforeAuctionLaunchInternalOutput; FinalizeBatchAuction_input finalizeBatchAuctionInput; FinalizeBatchAuction_output finalizeBatchAuctionOutput; FinalizeStandardAuction_input finalizeStandardAuctionInput; @@ -1144,6 +1205,9 @@ struct NOST : public ContractBase 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().auctionTimerPauseStartedAt.setInvalid(); + state.mut().auctionTimerPauseEndsAt.setInvalid(); 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, @@ -1188,6 +1252,9 @@ struct NOST : public ContractBase } state.mut().isPostBeginEpochPauseArmed = 1; + state.mut().isAuctionTimerPaused = 1; + state.mut().auctionTimerPauseStartedAt.setInvalid(); + state.mut().auctionTimerPauseEndsAt.setInvalid(); } END_EPOCH() @@ -1204,18 +1271,10 @@ struct NOST : public ContractBase return; } - if (state.get().isPostBeginEpochPauseArmed) + CALL(SyncAuctionPauseState, locals.syncAuctionPauseStateInput, locals.syncAuctionPauseStateOutput); + if (state.get().isAuctionTimerPaused) { - CALL(GetTicksBeforeAuctionLaunchInternal, locals.getTicksBeforeAuctionLaunchInternalInput, - locals.getTicksBeforeAuctionLaunchInternalOutput); - if (locals.getTicksBeforeAuctionLaunchInternalOutput.ticks == 0) - { - state.mut().isPostBeginEpochPauseArmed = 0; - } - else - { - return; - } + return; } locals.currentDate = qpi.now(); @@ -1300,44 +1359,115 @@ struct NOST : public ContractBase output.isValid = output.lotItemCount > 0 ? 1 : 0; } - PRIVATE_FUNCTION_WITH_LOCALS(IsAuctionInteractionPaused) + PRIVATE_FUNCTION_WITH_LOCALS(GetAuctionPauseState) { output.isPaused = 0; + output.pauseStartedAt.setInvalid(); + output.pauseEndsAt.setInvalid(); + 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; } - if (state.get().isPostBeginEpochPauseArmed && max(static_cast(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) - - (static_cast(qpi.tick()) - static_cast(qpi.initialTick())), - 0) > 0) + if (state.get().isPostBeginEpochPauseArmed && + (static_cast(qpi.tick()) - static_cast(qpi.initialTick())) < NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) + { + locals.elapsedTicksSinceInitialTick = static_cast(qpi.tick()) - static_cast(qpi.initialTick()); + locals.pauseStartedAt = locals.currentDate; + locals.pauseStartedAt.addMillisec( + -static_cast(smul(locals.elapsedTicksSinceInitialTick, static_cast(TARGET_TICK_DURATION)))); + locals.pauseEndsAt = locals.pauseStartedAt; + locals.pauseEndsAt.addMillisec(static_cast( + smul(static_cast(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS), static_cast(TARGET_TICK_DURATION)))); + accumulatePauseWindow(output.isPaused, output.pauseStartedAt, output.pauseEndsAt, locals.pauseStartedAt, locals.pauseEndsAt); + } + + if (qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()) == 0 && qpi.hour() == 11 && qpi.minute() >= 30) { output.isPaused = 1; - return; + output.pauseStartedAt = locals.currentDate; + output.pauseStartedAt.setTime(11, 30, 0, 0, 0); + output.pauseEndsAt = locals.currentDate; + output.pauseEndsAt.setTime(12, 0, 0, 0, 0); } + } + + PRIVATE_FUNCTION(IsAuctionInteractionPaused) { output.isPaused = state.get().isAuctionTimerPaused; } - if (qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()) != 0) + PRIVATE_PROCEDURE_WITH_LOCALS(SyncAuctionPauseState) + { + locals.currentDate = qpi.now(); + if (state.get().isPostBeginEpochPauseArmed && + (static_cast(qpi.tick()) - static_cast(qpi.initialTick())) >= NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) { - return; + state.mut().isPostBeginEpochPauseArmed = 0; } - if (qpi.hour() < 11 || qpi.hour() > 11) + CALL(GetAuctionPauseState, locals.getAuctionPauseStateInput, locals.getAuctionPauseStateOutput); + + 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 (qpi.minute() < 30) + if (!state.get().isAuctionTimerPaused) { return; } - output.isPaused = 1; + 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.status == EAuctionStatus::Active) + { + locals.auction.auctionDurationSeconds = sadd(locals.auction.auctionDurationSeconds, locals.pausedSeconds); + state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); + } + else if (locals.auction.status == EAuctionStatus::PendingSellerDecision && locals.auction.sellerDecisionDeadline.isValid()) + { + locals.auction.sellerDecisionDeadline.add(0, 0, 0, 0, 0, static_cast(locals.pausedSeconds)); + state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); + } + locals.auctionIndex = state.get().auctionList.nextElementIndex(locals.auctionIndex); + } + } + + state.mut().isAuctionTimerPaused = 0; + state.mut().auctionTimerPauseStartedAt.setInvalid(); + state.mut().auctionTimerPauseEndsAt.setInvalid(); } - PRIVATE_FUNCTION(GetTicksBeforeAuctionLaunchInternal) + PRIVATE_FUNCTION_WITH_LOCALS(GetTicksBeforeAuctionLaunchInternal) { output.ticks = 0; @@ -2521,6 +2651,13 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); + if (locals.isAuctionInteractionPausedOutput.isPaused) + { + output.errorCode = static_cast(EAuctionError::AuctionPaused); + return; + } + if (input.acceptSale > 1) { return; @@ -2545,7 +2682,7 @@ struct NOST : public ContractBase } locals.currentDate = qpi.now(); - if (locals.auction.sellerDecisionDeadline <= locals.currentDate) + if (!state.get().isAuctionTimerPaused && locals.auction.sellerDecisionDeadline <= locals.currentDate) { locals.finalizeStandardAuctionInput.auctionId = input.auctionId; locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; @@ -2698,7 +2835,7 @@ struct NOST : public ContractBase * @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(GetTicksBeforeAuctionLaunch) + PUBLIC_FUNCTION_WITH_LOCALS(GetTicksBeforeAuctionLaunch) { output.ticks = 0; @@ -2890,6 +3027,27 @@ struct NOST : public ContractBase static void makeDateStamp(uint8 year, uint8 month, uint8 day, uint32& res) { res = static_cast(year << 9 | month << 5 | day); } + static void accumulatePauseWindow(uint8& hasPauseWindow, DateAndTime& pauseStartedAt, DateAndTime& pauseEndsAt, + const DateAndTime& candidatePauseStartedAt, const DateAndTime& candidatePauseEndsAt) + { + if (!hasPauseWindow) + { + hasPauseWindow = 1; + pauseStartedAt = candidatePauseStartedAt; + pauseEndsAt = candidatePauseEndsAt; + return; + } + + if (candidatePauseStartedAt < pauseStartedAt) + { + pauseStartedAt = candidatePauseStartedAt; + } + if (candidatePauseEndsAt > pauseEndsAt) + { + pauseEndsAt = candidatePauseEndsAt; + } + } + /** * @brief Compares two Nostromo timestamps. * @param a Left-hand date-time. diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index 3c25b0842..3636ad8cf 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -21,6 +21,7 @@ namespace 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) @@ -1107,6 +1108,35 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) EXPECT_EQ(nostromo.managedShares(asset, seller), 1); } +TEST(ContractNostromoAuction, WeeklyPauseShiftsActiveAuctionDeadlineAuction) +{ + ContractTestingNostromoAuctionFromScratch 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, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( + ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + + nostromo.setNow(2026, 1, 7, 11, 40, 0); + nostromo.advanceAndEndTick(0); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Active); + + nostromo.setNow(2026, 1, 7, 12, 0, 0); + nostromo.advanceAndEndTick(0); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Active); + + nostromo.setNow(2026, 1, 7, 12, 10, 1); + nostromo.advanceAndEndTick(0); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.managedShares(asset, seller), 1); +} + TEST(ContractNostromoAuction, EndTickSkipsAuctionProcessingAtBootstrapTimeAuction) { ContractTestingNostromoAuctionFromScratch nostromo; @@ -1131,6 +1161,55 @@ TEST(ContractNostromoAuction, EndTickSkipsAuctionProcessingAtBootstrapTimeAuctio EXPECT_EQ(nostromo.managedShares(asset, seller), 0); } +TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) +{ + ContractTestingNostromoAuctionFromScratch 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, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( + ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY) * 1000ULL); + auto auction = nostromo.getAuction(createOutput.auctionId).auction; + ASSERT_EQ(auction.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(auction.sellerDecisionDeadline.getHour(), 9); + EXPECT_EQ(auction.sellerDecisionDeadline.getMinute(), 0); + EXPECT_EQ(auction.sellerDecisionDeadline.getSecond(), 0); + + nostromo.setNow(2026, 1, 9, 8, 59, 50); + nostromo.beginEpoch(); + EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + + nostromo.advanceAndEndTick(1000); + EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS - 1); + + nostromo.setNow(2026, 1, 9, 9, 8, 10); + nostromo.advanceAndEndTick(0); + auction = nostromo.getAuction(createOutput.auctionId).auction; + ASSERT_EQ(auction.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(auction.sellerDecisionDeadline.getHour(), 9); + EXPECT_EQ(auction.sellerDecisionDeadline.getMinute(), 8); + EXPECT_EQ(auction.sellerDecisionDeadline.getSecond(), 21); + + nostromo.setNow(2026, 1, 9, 9, 8, 20); + nostromo.advanceAndEndTick(0); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::PendingSellerDecision); + + nostromo.setNow(2026, 1, 9, 9, 8, 22); + nostromo.advanceAndEndTick(0); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); +} + TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) { ContractTestingNostromoAuctionFromScratch nostromo; From 0862707ec93fc6fd80d450b9d497b6b6dced2745 Mon Sep 17 00:00:00 2001 From: N-010 Date: Sat, 18 Apr 2026 00:02:46 +0300 Subject: [PATCH 26/59] Refactor `ContractTestingNostromoAuctionFromScratch` to `ContractTestingNOST`: simplify test class structure, introduce `NOSTChecker` for state interactions, and streamline auction-related helper functions. --- src/contracts/Nostromo.h | 276 ++++++--- test/contract_nostromo.cpp | 1190 +++++++++++++++++++----------------- 2 files changed, 840 insertions(+), 626 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 1ed32e263..cc89beaad 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -9,6 +9,7 @@ namespace QPI } // namespace QPI constexpr uint64 NOST_AUCTION_NUM = 2048; +constexpr uint64 NOST_AUCTION_HISTORY_NUM = 1024; constexpr uint64 NOST_AUCTION_METADATA_CID_LENGTH = 64; constexpr uint64 NOST_AUCTION_PARTICIPANT_NUM = 4096; constexpr uint64 NOST_AUCTION_LOT_ITEM_NUM = 64; @@ -34,6 +35,7 @@ constexpr uint64 NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS = 604800ULL; constexpr uint64 NOST_AUCTION_PRE_EPOCH_PAUSE_SECONDS = 1800ULL; constexpr uint32 NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS = 500U; constexpr uint32 NOST_DEFAULT_INIT_TIME = 22 << 9 | 4 << 5 | 13; +constexpr uint8 NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT = 1; struct NOST2 { @@ -260,18 +262,30 @@ struct NOST : public ContractBase /** @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 Circular buffer with identifiers of finalized and cancelled auctions. */ + Array closedAuctionHistory; + + /** @brief Monotonic insertion counter for `closedAuctionHistory`. */ + uint64 closedAuctionHistoryCounter; + HashMap auctionList; HashMap participants; }; @@ -560,6 +574,37 @@ struct NOST : public ContractBase uint64 shareholderFeeBasisPointsTier4; }; + /** + * @brief Pure breakdown of one auction revenue split. + * @note Runtime settlement and tests share this struct to keep fee arithmetic aligned. + */ + struct AuctionRevenueBreakdown + { + /** @brief Net amount that remains for the seller after every configured fee is applied. */ + uint64 sellerPayout; + + /** @brief Shareholder fee tier selected for the provided gross amount. */ + uint64 shareholderFeeBasisPoints; + + /** @brief Gross shareholder fee amount before dividend retention is split out. */ + uint64 shareholderFeeAmount; + + /** @brief Portion of the shareholder fee retained by the contract for dividend distribution. */ + uint64 shareholderDividendAmount; + + /** @brief Management wallet fee amount. */ + uint64 managementFeeAmount; + + /** @brief Development wallet fee amount. */ + uint64 developmentFeeAmount; + + /** @brief Base takeover coordinator fee charged directly from the gross amount. */ + uint64 takeoverCoordinatorBaseAmount; + + /** @brief Total takeover coordinator gain including retained shareholder-fee remainder. */ + uint64 takeoverCoordinatorFeeAmount; + }; + /** @brief Input payload used to read the wallets that receive auction fee transfers. */ using GetFeeRecipients_input = NoData; @@ -575,6 +620,27 @@ struct NOST : public ContractBase 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 identifiers recorded after finalization or cancellation. */ + Array auctionIds; + + /** @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; + }; + /** @brief Internal input used to validate an auction lot and resolve its total escrow quantity. */ struct AnalyzeAuctionLot_input { @@ -753,9 +819,6 @@ struct NOST : public ContractBase { /** @brief Compact current date marker used to detect the bootstrap default time sentinel. */ DateAndTime currentDate; - DateAndTime pauseStartedAt; - DateAndTime pauseEndsAt; - uint64 elapsedTicksSinceInitialTick; uint32 currentDateStamp; }; @@ -994,7 +1057,7 @@ struct NOST : public ContractBase AuctionLotEntry lotItem; uint64 lotItemIndex; uint64 rollbackLotItemIndex; - sint64 transferredShares; + sint64 remainingShares; }; /** @brief Internal input used to return an auction lot from contract escrow to a target wallet. */ @@ -1053,12 +1116,7 @@ struct NOST : public ContractBase struct DistributeAuctionRevenue_locals { - uint64 shareholderFeeBasisPoints; - uint64 shareholderFeeAmount; - uint64 managementFeeAmount; - uint64 developmentFeeAmount; - uint64 takeoverCoordinatorFeeAmount; - uint64 shareholderDividendAmount; + AuctionRevenueBreakdown auctionRevenueBreakdown; uint64 distributedDividendAmount; uint64 dividendPerShare; }; @@ -1154,6 +1212,12 @@ struct NOST : public ContractBase FinalizeStandardAuction_output finalizeStandardAuctionOutput; }; + struct BEGIN_EPOCH_locals + { + QX::Fees_input feesInput; + QX::Fees_output feesOutput; + }; + /** @brief Input payload used to move share management rights to another managing contract. */ struct TransferShareManagementRights_input { @@ -1190,6 +1254,8 @@ struct NOST : public ContractBase REGISTER_USER_FUNCTION(GetTicksBeforeAuctionLaunch, 3); REGISTER_USER_FUNCTION(GetAuctionFees, 4); REGISTER_USER_FUNCTION(GetFeeRecipients, 5); + REGISTER_USER_FUNCTION(GetClosedAuctionHistory, 6); + REGISTER_USER_FUNCTION(GetRouteAllFeesToDevelopment, 7); } INITIALIZE() @@ -1206,6 +1272,7 @@ struct NOST : public ContractBase 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().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, @@ -1223,7 +1290,7 @@ struct NOST : public ContractBase output.allowTransfer = true; } - BEGIN_EPOCH() + BEGIN_EPOCH_WITH_LOCALS() { // TODO: Change to valid epoch if (qpi.epoch() == 220) @@ -1240,6 +1307,7 @@ struct NOST : public ContractBase 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().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); @@ -1251,10 +1319,13 @@ struct NOST : public ContractBase _E, _N, _Z, _O, _X, _S, _V, _O, _B, _K, _G, _Z, _C, _C, _F, _D, _B, _D, _M, _T, _M, _L, _C); } + CALL_OTHER_CONTRACT_FUNCTION(QX, Fees, locals.feesInput, locals.feesOutput); + if (interContractCallError == NoCallError) + { + state.mut().qxTransferFee = locals.feesOutput.transferFee; + } + state.mut().isPostBeginEpochPauseArmed = 1; - state.mut().isAuctionTimerPaused = 1; - state.mut().auctionTimerPauseStartedAt.setInvalid(); - state.mut().auctionTimerPauseEndsAt.setInvalid(); } END_EPOCH() @@ -1266,10 +1337,6 @@ struct NOST : public ContractBase END_TICK_WITH_LOCALS() { makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentDateStamp); - if (locals.currentDateStamp == NOST_DEFAULT_INIT_TIME) - { - return; - } CALL(SyncAuctionPauseState, locals.syncAuctionPauseStateInput, locals.syncAuctionPauseStateOutput); if (state.get().isAuctionTimerPaused) @@ -1377,19 +1444,6 @@ struct NOST : public ContractBase return; } - if (state.get().isPostBeginEpochPauseArmed && - (static_cast(qpi.tick()) - static_cast(qpi.initialTick())) < NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) - { - locals.elapsedTicksSinceInitialTick = static_cast(qpi.tick()) - static_cast(qpi.initialTick()); - locals.pauseStartedAt = locals.currentDate; - locals.pauseStartedAt.addMillisec( - -static_cast(smul(locals.elapsedTicksSinceInitialTick, static_cast(TARGET_TICK_DURATION)))); - locals.pauseEndsAt = locals.pauseStartedAt; - locals.pauseEndsAt.addMillisec(static_cast( - smul(static_cast(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS), static_cast(TARGET_TICK_DURATION)))); - accumulatePauseWindow(output.isPaused, output.pauseStartedAt, output.pauseEndsAt, locals.pauseStartedAt, locals.pauseEndsAt); - } - if (qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()) == 0 && qpi.hour() == 11 && qpi.minute() >= 30) { output.isPaused = 1; @@ -1400,13 +1454,21 @@ struct NOST : public ContractBase } } - PRIVATE_FUNCTION(IsAuctionInteractionPaused) { output.isPaused = state.get().isAuctionTimerPaused; } + PRIVATE_FUNCTION(IsAuctionInteractionPaused) + { + 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; + } PRIVATE_PROCEDURE_WITH_LOCALS(SyncAuctionPauseState) { locals.currentDate = qpi.now(); - if (state.get().isPostBeginEpochPauseArmed && - (static_cast(qpi.tick()) - static_cast(qpi.initialTick())) >= NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) + if (state.get().isPostBeginEpochPauseArmed && (qpi.tick() - qpi.initialTick()) >= NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) { state.mut().isPostBeginEpochPauseArmed = 0; } @@ -1428,8 +1490,7 @@ struct NOST : public ContractBase { state.mut().auctionTimerPauseStartedAt = locals.getAuctionPauseStateOutput.pauseStartedAt; } - if (!state.get().auctionTimerPauseEndsAt.isValid() || - locals.getAuctionPauseStateOutput.pauseEndsAt > state.get().auctionTimerPauseEndsAt) + if (!state.get().auctionTimerPauseEndsAt.isValid() || locals.getAuctionPauseStateOutput.pauseEndsAt > state.get().auctionTimerPauseEndsAt) { state.mut().auctionTimerPauseEndsAt = locals.getAuctionPauseStateOutput.pauseEndsAt; } @@ -1441,6 +1502,14 @@ struct NOST : public ContractBase return; } + if (!state.get().auctionTimerPauseStartedAt.isValid() || !state.get().auctionTimerPauseEndsAt.isValid()) + { + state.mut().isAuctionTimerPaused = 0; + state.mut().auctionTimerPauseStartedAt.setInvalid(); + state.mut().auctionTimerPauseEndsAt.setInvalid(); + return; + } + diffDateInSecond(state.get().auctionTimerPauseStartedAt, state.get().auctionTimerPauseEndsAt, locals.pausedSeconds); if (locals.pausedSeconds > 0) { @@ -1492,35 +1561,39 @@ struct NOST : public ContractBase return; } - locals.shareholderFeeBasisPoints = getAuctionShareholderFeeBasisPoints(input.grossAmount, state); - locals.shareholderFeeAmount = div(smul(input.grossAmount, locals.shareholderFeeBasisPoints), 10000ULL); - locals.managementFeeAmount = div(smul(input.grossAmount, state.get().managementFeeBasisPoints), 10000ULL); - locals.developmentFeeAmount = div(smul(input.grossAmount, state.get().developmentFeeBasisPoints), 10000ULL); - locals.shareholderDividendAmount = div(smul(locals.shareholderFeeAmount, state.get().shareholderDividendBasisPoints), 10000ULL); - locals.takeoverCoordinatorFeeAmount = div(smul(input.grossAmount, state.get().takeoverCoordinatorFeeBasisPoints), 10000ULL) + - (locals.shareholderFeeAmount - locals.shareholderDividendAmount); - output.sellerPayout = input.grossAmount - locals.shareholderFeeAmount - locals.managementFeeAmount - locals.developmentFeeAmount - - div(smul(input.grossAmount, state.get().takeoverCoordinatorFeeBasisPoints), 10000ULL); + calculateAuctionRevenueBreakdown(input.grossAmount, state, locals.auctionRevenueBreakdown); + output.sellerPayout = locals.auctionRevenueBreakdown.sellerPayout; - state.mut().auctionShareholderDividendPool = sadd(state.get().auctionShareholderDividendPool, locals.shareholderDividendAmount); - if (locals.managementFeeAmount > 0) - { - qpi.transfer(state.get().management, locals.managementFeeAmount); - } - if (locals.developmentFeeAmount > 0) + if (routeAllFeesToDevelopment(state)) { - qpi.transfer(state.get().development, locals.developmentFeeAmount); + if (input.grossAmount > output.sellerPayout) + { + qpi.transfer(state.get().development, input.grossAmount - output.sellerPayout); + } } - if (locals.takeoverCoordinatorFeeAmount > 0) + else { - qpi.transfer(state.get().takeoverCoordinator, locals.takeoverCoordinatorFeeAmount); - } + state.mut().auctionShareholderDividendPool = + sadd(state.get().auctionShareholderDividendPool, locals.auctionRevenueBreakdown.shareholderDividendAmount); + if (locals.auctionRevenueBreakdown.managementFeeAmount > 0) + { + qpi.transfer(state.get().management, locals.auctionRevenueBreakdown.managementFeeAmount); + } + if (locals.auctionRevenueBreakdown.developmentFeeAmount > 0) + { + qpi.transfer(state.get().development, locals.auctionRevenueBreakdown.developmentFeeAmount); + } + if (locals.auctionRevenueBreakdown.takeoverCoordinatorFeeAmount > 0) + { + qpi.transfer(state.get().takeoverCoordinator, locals.auctionRevenueBreakdown.takeoverCoordinatorFeeAmount); + } - 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; + 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; + } } output.success = 1; @@ -2047,6 +2120,7 @@ struct NOST : public ContractBase locals.auction.status = EAuctionStatus::Finalized; locals.auction.settledAt = input.currentDate; state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); + addClosedAuctionToHistory(state, locals.auction.auctionId); output.success = 1; } @@ -2110,6 +2184,7 @@ struct NOST : public ContractBase locals.auction.highestBidder = NULL_ID; } state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); + addClosedAuctionToHistory(state, locals.auction.auctionId); output.success = 1; } @@ -2157,6 +2232,7 @@ struct NOST : public ContractBase locals.auction.status = EAuctionStatus::Finalized; locals.auction.settledAt = input.currentDate; state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); + addClosedAuctionToHistory(state, locals.auction.auctionId); output.success = 1; } @@ -2171,15 +2247,12 @@ struct NOST : public ContractBase continue; } - locals.transferredShares = qpi.transferShareOwnershipAndPossession(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, - qpi.invocator(), qpi.invocator(), locals.lotItem.quantity, SELF); - if (locals.transferredShares < locals.lotItem.quantity) + locals.remainingShares = qpi.transferShareOwnershipAndPossession(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, + qpi.invocator(), qpi.invocator(), locals.lotItem.quantity, SELF); + if (locals.remainingShares < 0) { - if (locals.transferredShares > 0) - { - qpi.transferShareOwnershipAndPossession(locals.lotItem.asset.assetName, locals.lotItem.asset.issuer, SELF, SELF, - locals.transferredShares, qpi.invocator()); - } + // `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); @@ -2403,7 +2476,7 @@ struct NOST : public ContractBase if (qpi.invocationReward() > locals.requiredFee) { - qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - locals.requiredFee); + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.requiredFee); } output.auctionId = locals.auction.auctionId; @@ -2628,6 +2701,7 @@ struct NOST : public ContractBase locals.auction.status = EAuctionStatus::Cancelled; locals.auction.settledAt = locals.currentDate; state.mut().auctionList.replace(input.auctionId, locals.auction); + addClosedAuctionToHistory(state, locals.auction.auctionId); if (static_cast(qpi.invocationReward()) > output.cancellationFee) { @@ -2878,6 +2952,22 @@ struct NOST : public ContractBase output.takeoverCoordinator = state.get().takeoverCoordinator; } + /** + * @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(GetClosedAuctionHistory) + { + output.auctionIds = state.get().closedAuctionHistory; + output.totalEntries = state.get().closedAuctionHistoryCounter; + } + + /** + * @brief Returns whether the temporary fee override routes every fee to development. + */ + PUBLIC_FUNCTION(GetRouteAllFeesToDevelopment) { output.enabled = state.get().routeAllFeesToDevelopment; } + /** * @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. @@ -2903,7 +2993,7 @@ struct NOST : public ContractBase } if (qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, input.newManagingContractIndex, - input.newManagingContractIndex, 0) < 0) + input.newManagingContractIndex, state.get().qxTransferFee) < 0) { // error output.transferredNumberOfShares = 0; @@ -2991,21 +3081,45 @@ struct NOST : public ContractBase 10000ULL; } - static uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount, const ContractState& state) + static uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount, const StateData& state) { if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1) { - return state.get().shareholderFeeBasisPointsTier1; + return state.shareholderFeeBasisPointsTier1; } if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2) { - return state.get().shareholderFeeBasisPointsTier2; + return state.shareholderFeeBasisPointsTier2; } if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3) { - return state.get().shareholderFeeBasisPointsTier3; + return state.shareholderFeeBasisPointsTier3; } - return state.get().shareholderFeeBasisPointsTier4; + return state.shareholderFeeBasisPointsTier4; + } + + static uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount, const ContractState& state) + { + return getAuctionShareholderFeeBasisPoints(grossAmount, state.get()); + } + + /** + * @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) + { + output.sellerPayout = grossAmount; + output.shareholderFeeBasisPoints = getAuctionShareholderFeeBasisPoints(grossAmount, state); + output.shareholderFeeAmount = div(smul(grossAmount, output.shareholderFeeBasisPoints), 10000ULL); + output.shareholderDividendAmount = div(smul(output.shareholderFeeAmount, state.get().shareholderDividendBasisPoints), 10000ULL); + output.managementFeeAmount = div(smul(grossAmount, state.get().managementFeeBasisPoints), 10000ULL); + output.developmentFeeAmount = div(smul(grossAmount, state.get().developmentFeeBasisPoints), 10000ULL); + output.takeoverCoordinatorBaseAmount = div(smul(grossAmount, state.get().takeoverCoordinatorFeeBasisPoints), 10000ULL); + output.takeoverCoordinatorFeeAmount = + output.takeoverCoordinatorBaseAmount + (output.shareholderFeeAmount - output.shareholderDividendAmount); + output.sellerPayout = grossAmount - output.shareholderFeeAmount - output.managementFeeAmount - output.developmentFeeAmount - + output.takeoverCoordinatorBaseAmount; } static sint64 getCreateAuctionFee(EAuctionVisibility visibility, const ContractState& state) @@ -3025,6 +3139,18 @@ struct NOST : public ContractBase static bool isZeroAsset(const Asset& asset) { return asset.assetName == 0 && isZero(asset.issuer); } + /** @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; + } + + static void addClosedAuctionToHistory(QPI::ContractState& state, const id& auctionId) + { + state.mut().closedAuctionHistory.set(mod(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()), auctionId); + state.mut().closedAuctionHistoryCounter = sadd(state.get().closedAuctionHistoryCounter, 1ULL); + } + static void makeDateStamp(uint8 year, uint8 month, uint8 day, uint32& res) { res = static_cast(year << 9 | month << 5 | day); } static void accumulatePauseWindow(uint8& hasPauseWindow, DateAndTime& pauseStartedAt, DateAndTime& pauseEndsAt, diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index 3636ad8cf..b6eb08e11 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -8,424 +8,421 @@ namespace { static constexpr uint64 QX_ISSUE_ASSET_FEE = 1000000000ULL; static const id NOST_CONTRACT_ID(NOST_CONTRACT_INDEX, 0, 0, 0); +} // namespace + +class NOSTChecker : public NOST, public NOST::StateData +{ - class ContractTestingNostromoAuctionFromScratch : protected ContractTesting +public: + const QPI::ContractState& asState() const { - public: - ContractTestingNostromoAuctionFromScratch() - { - initEmptySpectrum(); - initEmptyUniverse(); - INIT_CONTRACT(NOST); - 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); - } + return *reinterpret_cast*>(static_cast(this)); + } - void ensureUser(const id& user, sint64 amount = 1000) - { - if (getBalance(user) == 0) - { - increaseEnergy(user, amount); - } - } + void calculateAuctionRevenueBreakdown(uint64 grossAmount, AuctionRevenueBreakdown& output) const + { + NOST::calculateAuctionRevenueBreakdown(grossAmount, asState(), output); + } - void seedUser(const id& user, sint64 amount = 2000000000LL) { increaseEnergy(user, amount); } + uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount) const { return NOST::getAuctionShareholderFeeBasisPoints(grossAmount, asState()); } +}; - 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(); - } +class ContractTestingNOST : protected ContractTesting +{ +public: + ContractTestingNOST() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(NOST); + system.initialTick = system.tick; + 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 advanceAndEndTick(uint64 milliseconds) - { - advanceTimeAndTick(milliseconds); - callSystemProcedure(NOST_CONTRACT_INDEX, END_TICK); - } + NOSTChecker* state() { return reinterpret_cast(contractStates[NOST_CONTRACT_INDEX]); } - void advanceTicks(uint32 count, uint64 millisecondsPerTick = 1000ULL) + void ensureUser(const id& user, sint64 amount = 1000) + { + if (getBalance(user) == 0) { - for (uint32 i = 0; i < count; ++i) - { - advanceAndEndTick(millisecondsPerTick); - } + increaseEnergy(user, amount); } + } - void beginEpoch() - { - ++system.epoch; - callSystemProcedure(NOST_CONTRACT_INDEX, BEGIN_EPOCH); - } + void seedUser(const id& user, sint64 amount = 2000000000LL) { increaseEnergy(user, amount); } - sint64 issueAsset(const id& issuer, uint64 assetName, sint64 numberOfShares) - { - QX::IssueAsset_input input{}; - QX::IssueAsset_output output{}; + 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(); + } - input.assetName = assetName; - input.numberOfShares = numberOfShares; - input.unitOfMeasurement = 0; - input.numberOfDecimalPlaces = 0; + void advanceAndEndTick(uint64 milliseconds) + { + advanceTimeAndTick(milliseconds); + callSystemProcedure(NOST_CONTRACT_INDEX, END_TICK); + } - seedUser(issuer, QX_ISSUE_ASSET_FEE + 1000); - invokeUserProcedure(QX_CONTRACT_INDEX, 1, input, output, issuer, QX_ISSUE_ASSET_FEE); - return output.issuedNumberOfShares; + void advanceTicks(uint32 count, uint64 millisecondsPerTick = 1000ULL) + { + for (uint32 i = 0; i < count; ++i) + { + advanceAndEndTick(millisecondsPerTick); } + } - sint64 transferShareManagementRightsToNostromo(const id& owner, const Asset& asset, sint64 numberOfShares) - { - QX::TransferShareManagementRights_input input{}; - QX::TransferShareManagementRights_output output{}; + void beginEpoch() + { + ++system.epoch; + callSystemProcedure(NOST_CONTRACT_INDEX, BEGIN_EPOCH); + } - input.asset = asset; - input.numberOfShares = numberOfShares; - input.newManagingContractIndex = NOST_CONTRACT_INDEX; + sint64 issueAsset(const id& issuer, uint64 assetName, sint64 numberOfShares) + { + QX::IssueAsset_input input{}; + QX::IssueAsset_output output{}; - invokeUserProcedure(QX_CONTRACT_INDEX, 9, input, output, owner, 0); - return output.transferredNumberOfShares; - } + input.assetName = assetName; + input.numberOfShares = numberOfShares; + input.unitOfMeasurement = 0; + input.numberOfDecimalPlaces = 0; - NOST::CreateAuction_output createAuction(const id& seller, const NOST::CreateAuction_input& input, sint64 reward = 0) - { - NOST::CreateAuction_output output{}; - if (reward > 0) - { - seedUser(seller, reward + 1000); - } - else - { - ensureUser(seller); - } - invokeUserProcedure(NOST_CONTRACT_INDEX, 1, input, output, seller, reward); - return output; - } + seedUser(issuer, QX_ISSUE_ASSET_FEE); + invokeUserProcedure(QX_CONTRACT_INDEX, 1, input, output, issuer, QX_ISSUE_ASSET_FEE); + return output.issuedNumberOfShares; + } - NOST::PlaceBid_output placeBid(const id& bidder, const id& auctionId, uint64 quantity, uint64 bidAmount, sint64 reward) - { - NOST::PlaceBid_input input{}; - NOST::PlaceBid_output output{}; + sint64 transferShareManagementRightsToNostromo(const id& owner, const Asset& asset, sint64 numberOfShares) + { + QX::TransferShareManagementRights_input input{}; + QX::TransferShareManagementRights_output output{}; - input.auctionId = auctionId; - input.quantity = quantity; - input.bidAmount = bidAmount; + input.asset = asset; + input.numberOfShares = numberOfShares; + input.newManagingContractIndex = NOST_CONTRACT_INDEX; - seedUser(bidder, reward + 1000); - invokeUserProcedure(NOST_CONTRACT_INDEX, 2, input, output, bidder, reward); - return output; - } + invokeUserProcedure(QX_CONTRACT_INDEX, 9, input, output, owner, 0); + return output.transferredNumberOfShares; + } - NOST::CancelAuction_output cancelAuction(const id& seller, const id& auctionId, sint64 reward) + NOST::CreateAuction_output createAuction(const id& seller, const NOST::CreateAuction_input& input, sint64 reward = 0) + { + NOST::CreateAuction_output output{}; + if (reward > 0) { - NOST::CancelAuction_input input{}; - NOST::CancelAuction_output output{}; - - input.auctionId = auctionId; - if (reward > 0) - { - seedUser(seller, reward + 1000); - } - else - { - ensureUser(seller); - } - invokeUserProcedure(NOST_CONTRACT_INDEX, 3, input, output, seller, reward); - return output; + seedUser(seller, reward); } - - NOST::TransferShareManagementRights_output transferManagedShares(const id& owner, const Asset& asset, sint64 numberOfShares, - uint32 contractIndex) + else { - NOST::TransferShareManagementRights_input input{}; - NOST::TransferShareManagementRights_output output{}; - - input.asset = asset; - input.numberOfShares = numberOfShares; - input.newManagingContractIndex = contractIndex; - - ensureUser(owner); - invokeUserProcedure(NOST_CONTRACT_INDEX, 4, input, output, owner, 0); - return output; + ensureUser(seller); } + invokeUserProcedure(NOST_CONTRACT_INDEX, 1, input, output, seller, reward); + return output; + } - NOST::ResolvePendingStandardAuction_output resolvePendingStandardAuction(const id& seller, const id& auctionId, bool acceptSale) - { - NOST::ResolvePendingStandardAuction_input input{}; - NOST::ResolvePendingStandardAuction_output output{}; + NOST::PlaceBid_output placeBid(const id& bidder, const id& auctionId, uint64 quantity, uint64 bidAmount, sint64 reward) + { + NOST::PlaceBid_input input{}; + NOST::PlaceBid_output output{}; - input.auctionId = auctionId; - input.acceptSale = acceptSale ? 1 : 0; + input.auctionId = auctionId; + input.quantity = quantity; + input.bidAmount = bidAmount; - ensureUser(seller); - invokeUserProcedure(NOST_CONTRACT_INDEX, 5, input, output, seller, 0); - return output; - } + seedUser(bidder, reward ); + invokeUserProcedure(NOST_CONTRACT_INDEX, 2, input, output, bidder, reward); + return output; + } - NOST::SetAuctionFees_output setAuctionFees(const id& caller, const NOST::SetAuctionFees_input& input) + NOST::CancelAuction_output cancelAuction(const id& seller, const id& auctionId, sint64 reward) + { + NOST::CancelAuction_input input{}; + NOST::CancelAuction_output output{}; + + input.auctionId = auctionId; + if (reward > 0) { - NOST::SetAuctionFees_output output{}; - ensureUser(caller); - invokeUserProcedure(NOST_CONTRACT_INDEX, 6, input, output, caller, 0); - return output; + seedUser(seller, reward ); } - - NOST::SetAuctionFeesByManagement_output setAuctionFeesByManagement(const id& caller, const NOST::SetAuctionFeesByManagement_input& input) + else { - NOST::SetAuctionFeesByManagement_output output{}; - ensureUser(caller); - invokeUserProcedure(NOST_CONTRACT_INDEX, 7, input, output, caller, 0); - return output; + ensureUser(seller); } + invokeUserProcedure(NOST_CONTRACT_INDEX, 3, input, output, seller, reward); + return output; + } - NOST::SetManagement_output setManagement(const id& caller, const id& management) - { - NOST::SetManagement_input input{}; - NOST::SetManagement_output output{}; + NOST::TransferShareManagementRights_output transferManagedShares(const id& owner, const Asset& asset, sint64 numberOfShares, uint32 contractIndex) + { + NOST::TransferShareManagementRights_input input{}; + NOST::TransferShareManagementRights_output output{}; + + input.asset = asset; + input.numberOfShares = numberOfShares; + input.newManagingContractIndex = contractIndex; + + syncCachedQxTransferFee(); + increaseEnergy(NOST_CONTRACT_ID, getCachedQxTransferFee()); + ensureUser(owner); + invokeUserProcedure(NOST_CONTRACT_INDEX, 4, input, output, owner, 0); + return output; + } - input.management = management; - ensureUser(caller); - invokeUserProcedure(NOST_CONTRACT_INDEX, 8, input, output, caller, 0); - return output; - } + NOST::ResolvePendingStandardAuction_output resolvePendingStandardAuction(const id& seller, const id& auctionId, bool acceptSale) + { + NOST::ResolvePendingStandardAuction_input input{}; + NOST::ResolvePendingStandardAuction_output output{}; - NOST::GetAuction_output getAuction(const id& auctionId) const - { - NOST::GetAuction_input input{}; - NOST::GetAuction_output output{}; + input.auctionId = auctionId; + input.acceptSale = acceptSale ? 1 : 0; - input.auctionId = auctionId; - callFunction(NOST_CONTRACT_INDEX, 1, input, output); - return output; - } + ensureUser(seller); + invokeUserProcedure(NOST_CONTRACT_INDEX, 5, input, output, seller, 0); + return output; + } - NOST::GetAuctionParticipant_output getParticipant(const id& auctionId, const id& participant) const - { - NOST::GetAuctionParticipant_input input{}; - NOST::GetAuctionParticipant_output 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; + } - input.auctionId = auctionId; - input.participant = participant; - callFunction(NOST_CONTRACT_INDEX, 2, input, output); - 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::GetTicksBeforeAuctionLaunch_output getTicksBeforeAuctionLaunch() const - { - NOST::GetTicksBeforeAuctionLaunch_input input{}; - NOST::GetTicksBeforeAuctionLaunch_output output{}; + NOST::SetManagement_output setManagement(const id& caller, const id& management) + { + NOST::SetManagement_input input{}; + NOST::SetManagement_output output{}; - callFunction(NOST_CONTRACT_INDEX, 3, input, output); - return output; - } + input.management = management; + ensureUser(caller); + invokeUserProcedure(NOST_CONTRACT_INDEX, 8, input, output, caller, 0); + return output; + } - NOST::GetAuctionFees_output getAuctionFees() const - { - NOST::GetAuctionFees_input input{}; - NOST::GetAuctionFees_output output{}; + NOST::GetAuction_output getAuction(const id& auctionId) const + { + NOST::GetAuction_input input{}; + NOST::GetAuction_output output{}; - callFunction(NOST_CONTRACT_INDEX, 4, input, output); - return output; - } + input.auctionId = auctionId; + callFunction(NOST_CONTRACT_INDEX, 1, input, output); + return output; + } - NOST::GetFeeRecipients_output getFeeRecipients() const - { - NOST::GetFeeRecipients_input input{}; - NOST::GetFeeRecipients_output output{}; + NOST::GetAuctionParticipant_output getParticipant(const id& auctionId, const id& participant) const + { + NOST::GetAuctionParticipant_input input{}; + NOST::GetAuctionParticipant_output output{}; - callFunction(NOST_CONTRACT_INDEX, 5, input, output); - return output; - } + input.auctionId = auctionId; + input.participant = participant; + callFunction(NOST_CONTRACT_INDEX, 2, input, output); + return output; + } - sint64 managedShares(const Asset& asset, const id& owner) const - { - return numberOfPossessedShares(asset.assetName, asset.issuer, owner, owner, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX); - } + NOST::GetTicksBeforeAuctionLaunch_output getTicksBeforeAuctionLaunch() const + { + NOST::GetTicksBeforeAuctionLaunch_input input{}; + NOST::GetTicksBeforeAuctionLaunch_output output{}; - sint64 sharesManagedBy(const Asset& asset, const id& owner, uint32 contractIndex) const - { - return numberOfPossessedShares(asset.assetName, asset.issuer, owner, owner, contractIndex, contractIndex); - } + callFunction(NOST_CONTRACT_INDEX, 3, input, output); + return output; + } - sint64 plainShares(const Asset& asset, const id& owner) const - { - return numberOfShares(asset, AssetOwnershipSelect::byOwner(owner), AssetPossessionSelect::byPossessor(owner)); - } + NOST::GetAuctionFees_output getAuctionFees() const + { + NOST::GetAuctionFees_input input{}; + NOST::GetAuctionFees_output output{}; - 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; - } + callFunction(NOST_CONTRACT_INDEX, 4, input, output); + return output; + } - static Array makeInvalidMetadataCidFirstChar() - { - auto cid = makeMetadataCid(); - cid.set(0, 'c'); - return cid; - } + NOST::GetFeeRecipients_output getFeeRecipients() const + { + NOST::GetFeeRecipients_input input{}; + NOST::GetFeeRecipients_output output{}; - static Array makeInvalidMetadataCidUppercase() - { - auto cid = makeMetadataCid(); - cid.set(5, 'A'); - return cid; - } + callFunction(NOST_CONTRACT_INDEX, 5, input, output); + return output; + } - static Array makeSingleLot(const Asset& asset, sint64 quantity) - { - Array lot{}; - NOST::AuctionLotEntry entry{}; + 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]); } - entry.asset = asset; - entry.quantity = quantity; - lot.set(0, entry); - return lot; - } + 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; } - static Array makeTwoAssetLot(const Asset& assetA, sint64 quantityA, const Asset& assetB, - sint64 quantityB) - { - Array lot{}; - NOST::AuctionLotEntry entryA{}; - NOST::AuctionLotEntry entryB{}; - - entryA.asset = assetA; - entryA.quantity = quantityA; - entryB.asset = assetB; - entryB.quantity = quantityB; - lot.set(0, entryA); - lot.set(1, entryB); - return lot; - } + sint64 managedShares(const Asset& asset, const id& owner) const + { + return numberOfPossessedShares(asset.assetName, asset.issuer, owner, owner, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX); + } - static Array makeAllowedWallets(std::initializer_list wallets) - { - Array allowed{}; - uint64 index = 0; - for (const auto& wallet : wallets) - { - allowed.set(index++, wallet); - } - return allowed; - } + sint64 sharesManagedBy(const Asset& asset, const id& owner, uint32 contractIndex) const + { + return numberOfPossessedShares(asset.assetName, asset.issuer, owner, owner, contractIndex, contractIndex); + } - static Array makeRequiredAccessAssets(std::initializer_list assets) - { - Array required{}; - uint64 index = 0; - for (const auto& asset : assets) - { - required.set(index++, asset); - } - return required; - } + sint64 plainShares(const Asset& asset, const id& owner) const + { + return numberOfShares(asset, AssetOwnershipSelect::byOwner(owner), AssetPossessionSelect::byPossessor(owner)); + } - static NOST::CreateAuction_input makeBatchAuctionInput(const Asset& asset, sint64 quantity, uint64 salePrice = 10) + static Array makeMetadataCid() + { + Array cid{}; + const char* cidText = "bafybeigdyrzt2a3x4m5n6p7qrstuvwx234567abcdefghijklmnopqrst"; + for (uint64 i = 0; cidText[i] != 0 && i < NOST_AUCTION_METADATA_CID_LENGTH; ++i) { - NOST::CreateAuction_input input{}; - input.metadataIpfsCid = makeMetadataCid(); - input.auctionLotItems = makeSingleLot(asset, quantity); - input.salePrice = salePrice; - input.durationDays = 1; - input.auctionType = static_cast(NOST::EAuctionType::Batch); - input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Public); - return input; + cid.set(i, static_cast(cidText[i])); } + return cid; + } - static NOST::CreateAuction_input makeStandardAuctionInput(const Array& lot, - uint64 initialPrice = 100, uint64 salePrice = 150, uint64 minimumBidIncrement = 10, - 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; - } + static Array makeInvalidMetadataCidFirstChar() + { + auto cid = makeMetadataCid(); + cid.set(0, 'c'); + return cid; + } - 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 Array makeInvalidMetadataCidUppercase() + { + auto cid = makeMetadataCid(); + cid.set(5, 'A'); + return cid; + } - 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 Array makeSingleLot(const Asset& asset, sint64 quantity) + { + Array lot{}; + NOST::AuctionLotEntry entry{}; - 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); - } - }; + entry.asset = asset; + entry.quantity = quantity; + lot.set(0, entry); + return lot; + } - uint64 expectedShareholderFeeBasisPoints(const NOST::GetAuctionFees_output& fees, uint64 grossAmount) + static Array makeTwoAssetLot(const Asset& assetA, sint64 quantityA, const Asset& assetB, + sint64 quantityB) { - if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1) - { - return fees.shareholderFeeBasisPointsTier1; - } - if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2) + Array lot{}; + NOST::AuctionLotEntry entryA{}; + NOST::AuctionLotEntry entryB{}; + + entryA.asset = assetA; + entryA.quantity = quantityA; + entryB.asset = assetB; + entryB.quantity = quantityB; + lot.set(0, entryA); + lot.set(1, entryB); + return lot; + } + + static Array makeAllowedWallets(std::initializer_list wallets) + { + Array allowed{}; + uint64 index = 0; + for (const auto& wallet : wallets) { - return fees.shareholderFeeBasisPointsTier2; + allowed.set(index++, wallet); } - if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3) + return allowed; + } + + static Array makeRequiredAccessAssets(std::initializer_list assets) + { + Array required{}; + uint64 index = 0; + for (const auto& asset : assets) { - return fees.shareholderFeeBasisPointsTier3; + required.set(index++, asset); } - return fees.shareholderFeeBasisPointsTier4; + return required; } - uint64 expectedSellerPayout(const NOST::GetAuctionFees_output& fees, uint64 grossAmount) + static NOST::CreateAuction_input makeBatchAuctionInput(const Asset& asset, sint64 quantity, uint64 salePrice = 10) { - const uint64 shareholderFeeAmount = grossAmount * expectedShareholderFeeBasisPoints(fees, grossAmount) / 10000ULL; - const uint64 managementFeeAmount = grossAmount * fees.managementFeeBasisPoints / 10000ULL; - const uint64 developmentFeeAmount = grossAmount * fees.developmentFeeBasisPoints / 10000ULL; - const uint64 takeoverCoordinatorBaseAmount = grossAmount * fees.takeoverCoordinatorFeeBasisPoints / 10000ULL; - return grossAmount - shareholderFeeAmount - managementFeeAmount - developmentFeeAmount - takeoverCoordinatorBaseAmount; + NOST::CreateAuction_input input{}; + input.metadataIpfsCid = makeMetadataCid(); + input.auctionLotItems = makeSingleLot(asset, quantity); + input.salePrice = salePrice; + input.durationDays = 1; + input.auctionType = static_cast(NOST::EAuctionType::Batch); + input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Public); + return input; } - uint64 expectedTakeoverCoordinatorGain(const NOST::GetAuctionFees_output& fees, uint64 grossAmount) + static NOST::CreateAuction_input makeStandardAuctionInput(const Array& lot, + uint64 initialPrice = 100, uint64 salePrice = 150, uint64 minimumBidIncrement = 10, + uint64 buyNowPrice = 0) { - const uint64 shareholderFeeAmount = grossAmount * expectedShareholderFeeBasisPoints(fees, grossAmount) / 10000ULL; - const uint64 shareholderDividendAmount = shareholderFeeAmount * fees.shareholderDividendBasisPoints / 10000ULL; - const uint64 takeoverCoordinatorBaseAmount = grossAmount * fees.takeoverCoordinatorFeeBasisPoints / 10000ULL; - return takeoverCoordinatorBaseAmount + (shareholderFeeAmount - shareholderDividendAmount); + 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; } - uint64 expectedDividendRetention(const NOST::GetAuctionFees_output& fees, uint64 grossAmount) + void calculateAuctionRevenueBreakdown(uint64 grossAmount, NOST::AuctionRevenueBreakdown& output) { - const uint64 shareholderFeeAmount = grossAmount * expectedShareholderFeeBasisPoints(fees, grossAmount) / 10000ULL; - return shareholderFeeAmount * fees.shareholderDividendBasisPoints / 10000ULL; + state()->calculateAuctionRevenueBreakdown(grossAmount, output); + } + + uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount) { return state()->getAuctionShareholderFeeBasisPoints(grossAmount); } + + 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); } -} // namespace + + 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); + } +}; TEST(ContractNostromoAuction, InitialStateAndGettersAuction) { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const auto fees = nostromo.getAuctionFees(); EXPECT_EQ(fees.privateAuctionFee, NOST_DEFAULT_PRIVATE_AUCTION_FEE); @@ -440,9 +437,9 @@ TEST(ContractNostromoAuction, InitialStateAndGettersAuction) EXPECT_EQ(fees.shareholderFeeBasisPointsTier4, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4); const auto recipients = nostromo.getFeeRecipients(); - EXPECT_EQ(recipients.management, ContractTestingNostromoAuctionFromScratch::managementWallet()); - EXPECT_EQ(recipients.development, ContractTestingNostromoAuctionFromScratch::developmentWallet()); - EXPECT_EQ(recipients.takeoverCoordinator, ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet()); + EXPECT_EQ(recipients.management, ContractTestingNOST::managementWallet()); + EXPECT_EQ(recipients.development, ContractTestingNOST::developmentWallet()); + EXPECT_EQ(recipients.takeoverCoordinator, ContractTestingNOST::takeoverCoordinatorWallet()); const id missingAuction(777, 0, 0, 0); const id missingParticipant(888, 0, 0, 0); @@ -455,6 +452,7 @@ TEST(ContractNostromoAuction, InitialStateAndGettersAuction) EXPECT_EQ(launchPause.ticks, 0U); 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); @@ -462,7 +460,7 @@ TEST(ContractNostromoAuction, InitialStateAndGettersAuction) TEST(ContractNostromoAuction, TransferShareManagementRightsAuction) { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id owner(1, 2, 3, 4); const uint64 assetName = assetNameFromString("NOSTTR"); const Asset asset{owner, assetName}; @@ -493,7 +491,7 @@ TEST(ContractNostromoAuction, TransferShareManagementRightsAuction) TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(11, 12, 13, 14); const uint64 assetName = assetNameFromString("CRTBTN"); const Asset asset{seller, assetName}; @@ -501,7 +499,7 @@ TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) EXPECT_EQ(nostromo.issueAsset(seller, assetName, 9), 9); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 9), 9); - auto input = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 9, 25); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 9, 25); const auto output = nostromo.createAuction(seller, input); ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); EXPECT_FALSE(isZero(output.auctionId)); @@ -525,7 +523,7 @@ TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) TEST(ContractNostromoAuction, CreateStandardBundleAuctionEscrowsMixedLotAuction) { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(21, 22, 23, 24); const uint64 assetNameA = assetNameFromString("CRTSTA"); const uint64 assetNameB = assetNameFromString("CRTSTB"); @@ -537,8 +535,7 @@ TEST(ContractNostromoAuction, CreateStandardBundleAuctionEscrowsMixedLotAuction) EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetA, 2), 2); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetB, 3), 3); - auto input = ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( - ContractTestingNostromoAuctionFromScratch::makeTwoAssetLot(assetA, 2, assetB, 3), 100, 150, 5); + auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeTwoAssetLot(assetA, 2, assetB, 3), 100, 150, 5); const auto output = nostromo.createAuction(seller, input); ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); @@ -561,7 +558,7 @@ TEST(ContractNostromoAuction, CreateStandardBundleAuctionEscrowsMixedLotAuction) TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction) { { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(31, 32, 33, 34); const id allowedBidder(35, 36, 37, 38); const uint64 assetName = assetNameFromString("PRIWAL"); @@ -570,9 +567,9 @@ TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction EXPECT_EQ(nostromo.issueAsset(seller, assetName, 4), 4); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 4), 4); - auto input = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 4, 12); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 4, 12); input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.allowedBidderWallets = ContractTestingNostromoAuctionFromScratch::makeAllowedWallets({allowedBidder}); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); @@ -584,7 +581,7 @@ TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction } { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(41, 42, 43, 44); const id gatedBidder(45, 46, 47, 48); const uint64 saleAssetName = assetNameFromString("PRIACC"); @@ -596,9 +593,9 @@ TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction EXPECT_EQ(nostromo.issueAsset(gatedBidder, gateAssetName, 1), 1); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, saleAsset, 5), 5); - auto input = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(saleAsset, 5, 20); + auto input = ContractTestingNOST::makeBatchAuctionInput(saleAsset, 5, 20); input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.requiredAccessAssets = ContractTestingNostromoAuctionFromScratch::makeRequiredAccessAssets({gateAsset}); + input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({gateAsset}); const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); @@ -611,9 +608,48 @@ TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction } } +TEST(ContractNostromoAuction, PrivateAuctionFeeRemainsOnContractInBothModesAuction) +{ + const uint8 routeModes[] = {0, 1}; + for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++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); + + 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, static_cast(NOST::EAuctionError::Success)); + + 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, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + } +} + TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(51, 52, 53, 54); const id altIssuer(55, 56, 57, 58); const uint64 assetNameA = assetNameFromString("INVAAA"); @@ -627,79 +663,73 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetB, 5), 5); EXPECT_EQ(nostromo.issueAsset(altIssuer, assetNameFromString("GATINV"), 1), 1); - auto invalidCid = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); - invalidCid.metadataIpfsCid = ContractTestingNostromoAuctionFromScratch::makeInvalidMetadataCidFirstChar(); + auto invalidCid = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + invalidCid.metadataIpfsCid = ContractTestingNOST::makeInvalidMetadataCidFirstChar(); EXPECT_EQ(nostromo.createAuction(seller, invalidCid).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); - auto invalidCidUppercase = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); - invalidCidUppercase.metadataIpfsCid = ContractTestingNostromoAuctionFromScratch::makeInvalidMetadataCidUppercase(); + auto invalidCidUppercase = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + invalidCidUppercase.metadataIpfsCid = ContractTestingNOST::makeInvalidMetadataCidUppercase(); EXPECT_EQ(nostromo.createAuction(seller, invalidCidUppercase).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); - auto emptyLot = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + auto emptyLot = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); emptyLot.auctionLotItems = Array{}; EXPECT_EQ(nostromo.createAuction(seller, emptyLot).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); - auto negativeQuantity = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); - negativeQuantity.auctionLotItems = ContractTestingNostromoAuctionFromScratch::makeSingleLot(assetA, -1); + auto negativeQuantity = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + negativeQuantity.auctionLotItems = ContractTestingNOST::makeSingleLot(assetA, -1); EXPECT_EQ(nostromo.createAuction(seller, negativeQuantity).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); - auto zeroDuration = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + auto zeroDuration = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); zeroDuration.durationDays = 0; EXPECT_EQ(nostromo.createAuction(seller, zeroDuration).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); - auto tooLongDuration = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + auto tooLongDuration = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); tooLongDuration.durationDays = NOST_AUCTION_MAX_DURATION_DAYS + 1; EXPECT_EQ(nostromo.createAuction(seller, tooLongDuration).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); - auto invalidType = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + auto invalidType = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); invalidType.auctionType = 99; EXPECT_EQ(nostromo.createAuction(seller, invalidType).errorCode, static_cast(NOST::EAuctionError::InvalidAuctionType)); - auto invalidVisibility = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + auto invalidVisibility = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); invalidVisibility.auctionVisibility = 99; EXPECT_EQ(nostromo.createAuction(seller, invalidVisibility).errorCode, static_cast(NOST::EAuctionError::InvalidVisibility)); - auto invalidBatchBundle = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); - invalidBatchBundle.auctionLotItems = ContractTestingNostromoAuctionFromScratch::makeTwoAssetLot(assetA, 2, assetB, 3); + auto invalidBatchBundle = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + invalidBatchBundle.auctionLotItems = ContractTestingNOST::makeTwoAssetLot(assetA, 2, assetB, 3); EXPECT_EQ(nostromo.createAuction(seller, invalidBatchBundle).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); - auto invalidBatchBuyNow = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + auto invalidBatchBuyNow = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); invalidBatchBuyNow.buyNowPrice = 100; EXPECT_EQ(nostromo.createAuction(seller, invalidBatchBuyNow).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); - auto invalidStandardMinimumPurchase = - ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput(ContractTestingNostromoAuctionFromScratch::makeSingleLot(assetA, 1)); + auto invalidStandardMinimumPurchase = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); invalidStandardMinimumPurchase.minimumPurchaseQuantity = 2; EXPECT_EQ(nostromo.createAuction(seller, invalidStandardMinimumPurchase).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); - auto invalidStandardIncrement = - ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput(ContractTestingNostromoAuctionFromScratch::makeSingleLot(assetA, 1)); + auto invalidStandardIncrement = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); invalidStandardIncrement.minimumBidIncrement = 0; EXPECT_EQ(nostromo.createAuction(seller, invalidStandardIncrement).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); - auto invalidStandardPrice = ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( - ContractTestingNostromoAuctionFromScratch::makeSingleLot(assetA, 1), 200, 150, 10); + auto invalidStandardPrice = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1), 200, 150, 10); EXPECT_EQ(nostromo.createAuction(seller, invalidStandardPrice).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); - auto invalidStandardSalePrice = - ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput(ContractTestingNostromoAuctionFromScratch::makeSingleLot(assetA, 1)); + auto invalidStandardSalePrice = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); invalidStandardSalePrice.salePrice = 0; EXPECT_EQ(nostromo.createAuction(seller, invalidStandardSalePrice).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); - auto invalidStandardBuyNow = ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( - ContractTestingNostromoAuctionFromScratch::makeSingleLot(assetA, 1), 100, 150, 10, 140); + auto invalidStandardBuyNow = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1), 100, 150, 10, 140); EXPECT_EQ(nostromo.createAuction(seller, invalidStandardBuyNow).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); - auto privateWithoutGate = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + auto privateWithoutGate = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); privateWithoutGate.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); EXPECT_EQ(nostromo.createAuction(seller, privateWithoutGate, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); - auto privateWithBothGates = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(assetA, 5, 10); + auto privateWithBothGates = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); privateWithBothGates.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - privateWithBothGates.allowedBidderWallets = ContractTestingNostromoAuctionFromScratch::makeAllowedWallets({id(99, 1, 1, 1)}); - privateWithBothGates.requiredAccessAssets = - ContractTestingNostromoAuctionFromScratch::makeRequiredAccessAssets({Asset{altIssuer, assetNameFromString("GATINV")}}); + privateWithBothGates.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({id(99, 1, 1, 1)}); + privateWithBothGates.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({Asset{altIssuer, assetNameFromString("GATINV")}}); EXPECT_EQ(nostromo.createAuction(seller, privateWithBothGates, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); } @@ -707,7 +737,7 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientAssetBalanceAndPauseAuction) { { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(61, 62, 63, 64); const uint64 assetName = assetNameFromString("PRIFEE"); const Asset asset{seller, assetName}; @@ -715,9 +745,9 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientA EXPECT_EQ(nostromo.issueAsset(seller, assetName, 4), 4); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 4), 4); - auto input = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 4, 10); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 4, 10); input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.allowedBidderWallets = ContractTestingNostromoAuctionFromScratch::makeAllowedWallets({id(1, 1, 1, 1)}); + 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, static_cast(NOST::EAuctionError::InsufficientFunds)); @@ -725,7 +755,7 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientA } { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(71, 72, 73, 74); const uint64 assetName = assetNameFromString("BALLOW"); const Asset asset{seller, assetName}; @@ -733,14 +763,14 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientA EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); - auto input = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 3, 10); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 3, 10); const auto output = nostromo.createAuction(seller, input); EXPECT_EQ(output.errorCode, static_cast(NOST::EAuctionError::InsufficientAssetBalance)); EXPECT_EQ(nostromo.managedShares(asset, seller), 2); } { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(81, 82, 83, 84); const uint64 assetName = assetNameFromString("PAUSEA"); const Asset asset{seller, assetName}; @@ -748,8 +778,10 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientA EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); - auto input = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 3, 10); + 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, static_cast(NOST::EAuctionError::AuctionPaused)); EXPECT_TRUE(isZero(output.auctionId)); @@ -757,7 +789,7 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientA } { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(85, 86, 87, 88); const uint64 assetName = assetNameFromString("BOOTPA"); const Asset asset{seller, assetName}; @@ -765,8 +797,10 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientA EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); - auto input = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 3, 10); + 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, static_cast(NOST::EAuctionError::AuctionPaused)); EXPECT_TRUE(isZero(output.auctionId)); @@ -776,7 +810,7 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientA TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAuction) { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(91, 92, 93, 94); const id bidderA(95, 96, 97, 98); const id bidderB(99, 100, 101, 102); @@ -787,7 +821,7 @@ TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAucti EXPECT_EQ(nostromo.issueAsset(seller, assetName, 6), 6); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 6), 6); - const auto createOutput = nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 6, 10)); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 6, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); const auto sellerBid = nostromo.placeBid(seller, createOutput.auctionId, 1, 12, 12); @@ -840,7 +874,7 @@ TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAucti TEST(ContractNostromoAuction, PlaceBatchBidExtendsAuctionNearEndAuction) { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(111, 112, 113, 114); const id bidder(115, 116, 117, 118); const uint64 assetName = assetNameFromString("BIDEXT"); @@ -849,7 +883,7 @@ TEST(ContractNostromoAuction, PlaceBatchBidExtendsAuctionNearEndAuction) EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); - const auto createOutput = nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 2, 10)); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); nostromo.setNow(2026, 1, 2, 8, 56, 30); @@ -862,7 +896,7 @@ TEST(ContractNostromoAuction, PlaceBatchBidExtendsAuctionNearEndAuction) TEST(ContractNostromoAuction, PlaceStandardBidValidatesRefundsAndPauseAuction) { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(121, 122, 123, 124); const id bidderA(125, 126, 127, 128); const id bidderB(129, 130, 131, 132); @@ -872,8 +906,8 @@ TEST(ContractNostromoAuction, PlaceStandardBidValidatesRefundsAndPauseAuction) EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - const auto createOutput = nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( - ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); const auto sellerBid = nostromo.placeBid(seller, createOutput.auctionId, 1, 100, 100); @@ -917,6 +951,8 @@ TEST(ContractNostromoAuction, PlaceStandardBidValidatesRefundsAndPauseAuction) EXPECT_EQ(resumedBid.errorCode, static_cast(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.auctionId, 1, 150, 150); EXPECT_EQ(bootstrapPausedBid.errorCode, static_cast(NOST::EAuctionError::AuctionPaused)); } @@ -924,7 +960,7 @@ TEST(ContractNostromoAuction, PlaceStandardBidValidatesRefundsAndPauseAuction) TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) { { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(141, 142, 143, 144); const id allowed(145, 146, 147, 148); const id denied(149, 150, 151, 152); @@ -934,9 +970,9 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); - auto input = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 3, 10); + auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 3, 10); input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.allowedBidderWallets = ContractTestingNostromoAuctionFromScratch::makeAllowedWallets({allowed}); + input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowed}); const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); @@ -947,7 +983,7 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) } { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(153, 154, 155, 156); const id allowed(157, 158, 159, 160); const id denied(161, 162, 163, 164); @@ -960,9 +996,9 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) EXPECT_EQ(nostromo.issueAsset(allowed, accessAssetName, 1), 1); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, saleAsset, 3), 3); - auto input = ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(saleAsset, 3, 10); + auto input = ContractTestingNOST::makeBatchAuctionInput(saleAsset, 3, 10); input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.requiredAccessAssets = ContractTestingNostromoAuctionFromScratch::makeRequiredAccessAssets({accessAsset}); + input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({accessAsset}); const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); @@ -975,7 +1011,7 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(171, 172, 173, 174); const id bidder(175, 176, 177, 178); const uint64 assetNameA = assetNameFromString("BUYNWA"); @@ -988,10 +1024,10 @@ TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetA, 2), 2); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetB, 1), 1); - auto input = ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( - ContractTestingNostromoAuctionFromScratch::makeTwoAssetLot(assetA, 2, assetB, 1), 100, 150, 10, 180); + auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeTwoAssetLot(assetA, 2, assetB, 1), 100, 150, 10, 180); const sint64 sellerBalanceBefore = getBalance(seller); - const auto fees = nostromo.getAuctionFees(); + NOST::AuctionRevenueBreakdown expectedRevenue{}; + nostromo.calculateAuctionRevenueBreakdown(180ULL, expectedRevenue); const auto createOutput = nostromo.createAuction(seller, input); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); @@ -1008,13 +1044,13 @@ TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) EXPECT_EQ(participant.participantData.isWinningBid, 1u); EXPECT_EQ(nostromo.managedShares(assetA, bidder), 2); EXPECT_EQ(nostromo.managedShares(assetB, bidder), 1); - EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, expectedSellerPayout(fees, 180ULL)); + EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, expectedRevenue.sellerPayout); } TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialFillAuction) { { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(181, 182, 183, 184); const id bidderA(185, 186, 187, 188); const id bidderB(189, 190, 191, 192); @@ -1025,7 +1061,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF EXPECT_EQ(nostromo.issueAsset(seller, assetName, 4), 4); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 4), 4); - const auto createOutput = nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 4, 10)); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 4, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); ASSERT_EQ(nostromo.placeBid(bidderA, createOutput.auctionId, 3, 15, 45).errorCode, static_cast(NOST::EAuctionError::Success)); @@ -1062,7 +1098,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF } { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(197, 198, 199, 200); const id bidder(201, 202, 203, 204); const uint64 assetName = assetNameFromString("BATRET"); @@ -1071,7 +1107,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF EXPECT_EQ(nostromo.issueAsset(seller, assetName, 5), 5); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 5), 5); - const auto createOutput = nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 5, 10)); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 5, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 2, 12, 24).errorCode, static_cast(NOST::EAuctionError::Success)); @@ -1087,7 +1123,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(211, 212, 213, 214); const uint64 assetName = assetNameFromString("STDNOB"); const Asset asset{seller, assetName}; @@ -1095,8 +1131,8 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - const auto createOutput = nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( - ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); @@ -1110,7 +1146,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) TEST(ContractNostromoAuction, WeeklyPauseShiftsActiveAuctionDeadlineAuction) { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(215, 216, 217, 218); const uint64 assetName = assetNameFromString("PAUSHL"); const Asset asset{seller, assetName}; @@ -1119,8 +1155,8 @@ TEST(ContractNostromoAuction, WeeklyPauseShiftsActiveAuctionDeadlineAuction) EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - const auto createOutput = nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( - ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); nostromo.setNow(2026, 1, 7, 11, 40, 0); @@ -1139,7 +1175,7 @@ TEST(ContractNostromoAuction, WeeklyPauseShiftsActiveAuctionDeadlineAuction) TEST(ContractNostromoAuction, EndTickSkipsAuctionProcessingAtBootstrapTimeAuction) { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(215, 216, 217, 218); const uint64 assetName = assetNameFromString("BOOTTK"); const Asset asset{seller, assetName}; @@ -1148,8 +1184,8 @@ TEST(ContractNostromoAuction, EndTickSkipsAuctionProcessingAtBootstrapTimeAuctio EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - const auto createOutput = nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( - ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); nostromo.setNow(2022, 4, 13, 12, 0, 0); @@ -1163,7 +1199,7 @@ TEST(ContractNostromoAuction, EndTickSkipsAuctionProcessingAtBootstrapTimeAuctio TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(219, 220, 221, 222); const id bidder(223, 224, 225, 226); const uint64 assetName = assetNameFromString("PDSHFT"); @@ -1173,8 +1209,7 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); const auto createOutput = - nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( - ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); @@ -1212,46 +1247,62 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) { - ContractTestingNostromoAuctionFromScratch nostromo; - const id seller(221, 222, 223, 224); - const id bidder(225, 226, 227, 228); - const uint64 assetName = assetNameFromString("STDEND"); - const Asset asset{seller, assetName}; + const uint8 routeModes[] = {0, 1}; + for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++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}; - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + nostromo.setRouteAllFeesToDevelopment(routeMode); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - const auto fees = nostromo.getAuctionFees(); - const sint64 sellerBalanceBefore = getBalance(seller); - const sint64 managementBefore = getBalance(ContractTestingNostromoAuctionFromScratch::managementWallet()); - const sint64 developmentBefore = getBalance(ContractTestingNostromoAuctionFromScratch::developmentWallet()); - const sint64 coordinatorBefore = getBalance(ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet()); - const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + NOST::AuctionRevenueBreakdown expectedRevenue{}; + nostromo.calculateAuctionRevenueBreakdown(10000ULL, expectedRevenue); + 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); - const auto createOutput = - nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( - ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 10000, 10000, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 10000, 10000).errorCode, static_cast(NOST::EAuctionError::Success)); + const auto createOutput = nostromo.createAuction( + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 10000, 10000, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 10000, 10000).errorCode, static_cast(NOST::EAuctionError::Success)); - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; - EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(auction.allocatedQuantity, 1ULL); - EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); - EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, expectedSellerPayout(fees, 10000ULL)); - EXPECT_EQ(getBalance(ContractTestingNostromoAuctionFromScratch::managementWallet()) - managementBefore, 50); - EXPECT_EQ(getBalance(ContractTestingNostromoAuctionFromScratch::developmentWallet()) - developmentBefore, 50); - EXPECT_EQ(getBalance(ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet()) - coordinatorBefore, - expectedTakeoverCoordinatorGain(fees, 10000ULL)); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendRetention(fees, 10000ULL)); + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.allocatedQuantity, 1ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); + EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, expectedRevenue.sellerPayout); + + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 10000ULL - expectedRevenue.sellerPayout); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); + } + else + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedRevenue.managementFeeAmount); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedRevenue.developmentFeeAmount); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedRevenue.takeoverCoordinatorFeeAmount); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedRevenue.shareholderDividendAmount); + } + } } TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction) { { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(231, 232, 233, 234); const id bidder(235, 236, 237, 238); const uint64 assetName = assetNameFromString("PENACC"); @@ -1261,8 +1312,7 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); const auto createOutput = - nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( - ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); @@ -1279,7 +1329,7 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction } { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(239, 240, 241, 242); const id bidder(243, 244, 245, 246); const uint64 assetName = assetNameFromString("PENREJ"); @@ -1289,8 +1339,7 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); const auto createOutput = - nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( - ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); @@ -1311,7 +1360,7 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction } { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(247, 248, 249, 250); const id bidder(251, 252, 253, 254); const uint64 assetName = assetNameFromString("PENTMO"); @@ -1321,8 +1370,7 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); const auto createOutput = - nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( - ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); @@ -1342,62 +1390,85 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction TEST(ContractNostromoAuction, CancelAuctionRefundsAndChargesCorrectFeeAuction) { + const uint8 routeModes[] = {0, 1}; + for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) { - ContractTestingNostromoAuctionFromScratch nostromo; - const id seller(261, 262, 263, 264); - const id bidderA(265, 266, 267, 268); - const id bidderB(269, 270, 271, 272); - const uint64 assetName = assetNameFromString("CANBAT"); - const Asset asset{seller, assetName}; - const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 10), 10); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); - - const auto createOutput = nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 10, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - ASSERT_EQ(nostromo.placeBid(bidderA, createOutput.auctionId, 2, 20, 40).errorCode, static_cast(NOST::EAuctionError::Success)); - ASSERT_EQ(nostromo.placeBid(bidderB, createOutput.auctionId, 3, 15, 45).errorCode, static_cast(NOST::EAuctionError::Success)); - - const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionId, 10); - EXPECT_EQ(cancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - EXPECT_EQ(cancelOutput.refundedAmount, 85ULL); - EXPECT_EQ(cancelOutput.cancellationFee, 10ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Cancelled); - EXPECT_EQ(nostromo.managedShares(asset, seller), 10); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 10); - } - - { - ContractTestingNostromoAuctionFromScratch nostromo; - const id seller(273, 274, 275, 276); - const id bidder(277, 278, 279, 280); - const uint64 assetName = assetNameFromString("CANSTD"); - const Asset asset{seller, assetName}; - const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - - const auto createOutput = - nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( - ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), 100, 150, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); + { + ContractTestingNOST nostromo; + const uint8 routeMode = routeModes[routeIndex]; + const id seller(261 + routeIndex, 262 + routeIndex, 263 + routeIndex, 264 + routeIndex); + const id bidderA(265 + routeIndex, 266 + routeIndex, 267 + routeIndex, 268 + routeIndex); + const id bidderB(269 + routeIndex, 270 + routeIndex, 271 + routeIndex, 272 + routeIndex); + const uint64 assetName = assetNameFromString(routeMode ? "CANBT1" : "CANBT0"); + const Asset asset{seller, assetName}; + 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); + + 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, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidderA, createOutput.auctionId, 2, 20, 40).errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidderB, createOutput.auctionId, 3, 15, 45).errorCode, static_cast(NOST::EAuctionError::Success)); + + const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionId, 10); + EXPECT_EQ(cancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(cancelOutput.refundedAmount, 85ULL); + EXPECT_EQ(cancelOutput.cancellationFee, 10ULL); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.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, 10); + } - const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionId, 15); - EXPECT_EQ(cancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - EXPECT_EQ(cancelOutput.refundedAmount, 120ULL); - EXPECT_EQ(cancelOutput.cancellationFee, 15ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Cancelled); - EXPECT_EQ(nostromo.managedShares(asset, seller), 1); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 15); + { + ContractTestingNOST nostromo; + const uint8 routeMode = routeModes[routeIndex]; + const id seller(273 + routeIndex, 274 + routeIndex, 275 + routeIndex, 276 + routeIndex); + const id bidder(277 + routeIndex, 278 + routeIndex, 279 + routeIndex, 280 + routeIndex); + const uint64 assetName = assetNameFromString(routeMode ? "CANST1" : "CANST0"); + const Asset asset{seller, assetName}; + 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); + + 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), 100, 150, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); + + const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionId, 15); + EXPECT_EQ(cancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(cancelOutput.refundedAmount, 120ULL); + EXPECT_EQ(cancelOutput.cancellationFee, 15ULL); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.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, 15); + } } } TEST(ContractNostromoAuction, CancelAuctionRejectsInvalidCasesAuction) { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id seller(281, 282, 283, 284); const id bidder(285, 286, 287, 288); const uint64 assetName = assetNameFromString("CANINV"); @@ -1406,7 +1477,7 @@ TEST(ContractNostromoAuction, CancelAuctionRejectsInvalidCasesAuction) EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); - const auto createOutput = nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeBatchAuctionInput(asset, 2, 10)); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 12, 12).errorCode, static_cast(NOST::EAuctionError::Success)); @@ -1428,7 +1499,7 @@ TEST(ContractNostromoAuction, CancelAuctionRejectsInvalidCasesAuction) TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) { - ContractTestingNostromoAuctionFromScratch nostromo; + ContractTestingNOST nostromo; const id outsider(291, 292, 293, 294); const id newManagement(295, 296, 297, 298); @@ -1449,11 +1520,10 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) NOST::SetAuctionFees_input invalidCoordinatorInput = coordinatorInput; invalidCoordinatorInput.privateAuctionFee = -1; - const auto coordinatorInvalid = - nostromo.setAuctionFees(ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet(), invalidCoordinatorInput); + const auto coordinatorInvalid = nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), invalidCoordinatorInput); EXPECT_EQ(coordinatorInvalid.errorCode, static_cast(NOST::EAuctionError::InvalidInput)); - const auto coordinatorSuccess = nostromo.setAuctionFees(ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet(), coordinatorInput); + const auto coordinatorSuccess = nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput); EXPECT_EQ(coordinatorSuccess.errorCode, static_cast(NOST::EAuctionError::Success)); auto fees = nostromo.getAuctionFees(); @@ -1468,10 +1538,10 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) const auto setManagementForbidden = nostromo.setManagement(outsider, newManagement); EXPECT_EQ(setManagementForbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); - const auto setManagementInvalid = nostromo.setManagement(ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet(), NULL_ID); + const auto setManagementInvalid = nostromo.setManagement(ContractTestingNOST::takeoverCoordinatorWallet(), NULL_ID); EXPECT_EQ(setManagementInvalid.errorCode, static_cast(NOST::EAuctionError::InvalidInput)); - const auto setManagementSuccess = nostromo.setManagement(ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet(), newManagement); + const auto setManagementSuccess = nostromo.setManagement(ContractTestingNOST::takeoverCoordinatorWallet(), newManagement); EXPECT_EQ(setManagementSuccess.errorCode, static_cast(NOST::EAuctionError::Success)); EXPECT_EQ(nostromo.getFeeRecipients().management, newManagement); @@ -1485,8 +1555,7 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) managementInput.shareholderFeeBasisPointsTier3 = 200; managementInput.shareholderFeeBasisPointsTier4 = 150; - const auto oldManagementForbidden = - nostromo.setAuctionFeesByManagement(ContractTestingNostromoAuctionFromScratch::managementWallet(), managementInput); + const auto oldManagementForbidden = nostromo.setAuctionFeesByManagement(ContractTestingNOST::managementWallet(), managementInput); EXPECT_EQ(oldManagementForbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); NOST::SetAuctionFeesByManagement_input invalidManagementInput = managementInput; @@ -1527,39 +1596,58 @@ TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) {200000000001ULL, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4, assetNameFromString("TIERA4")}, }; + const uint8 routeModes[] = {0, 1}; for (uint64 caseIndex = 0; caseIndex < sizeof(cases) / sizeof(cases[0]); ++caseIndex) { - ContractTestingNostromoAuctionFromScratch nostromo; - const id seller(301 + caseIndex, 302 + caseIndex, 303 + caseIndex, 304 + caseIndex); - const id bidder(401 + caseIndex, 402 + caseIndex, 403 + caseIndex, 404 + caseIndex); - const Asset asset{seller, cases[caseIndex].assetName}; - const auto fees = nostromo.getAuctionFees(); - const sint64 sellerBefore = getBalance(seller); - const sint64 managementBefore = getBalance(ContractTestingNostromoAuctionFromScratch::managementWallet()); - const sint64 developmentBefore = getBalance(ContractTestingNostromoAuctionFromScratch::developmentWallet()); - const sint64 coordinatorBefore = getBalance(ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet()); - const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - - EXPECT_EQ(nostromo.issueAsset(seller, cases[caseIndex].assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - - const auto createOutput = nostromo.createAuction(seller, ContractTestingNostromoAuctionFromScratch::makeStandardAuctionInput( - ContractTestingNostromoAuctionFromScratch::makeSingleLot(asset, 1), - cases[caseIndex].grossAmount, cases[caseIndex].grossAmount, 1)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, cases[caseIndex].grossAmount, cases[caseIndex].grossAmount).errorCode, - static_cast(NOST::EAuctionError::Success)); - - nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - - EXPECT_EQ(expectedShareholderFeeBasisPoints(fees, cases[caseIndex].grossAmount), cases[caseIndex].expectedShareholderFeeBp); - EXPECT_EQ(getBalance(seller) - sellerBefore, expectedSellerPayout(fees, cases[caseIndex].grossAmount)); - EXPECT_EQ(getBalance(ContractTestingNostromoAuctionFromScratch::managementWallet()) - managementBefore, - cases[caseIndex].grossAmount * fees.managementFeeBasisPoints / 10000ULL); - EXPECT_EQ(getBalance(ContractTestingNostromoAuctionFromScratch::developmentWallet()) - developmentBefore, - cases[caseIndex].grossAmount * fees.developmentFeeBasisPoints / 10000ULL); - EXPECT_EQ(getBalance(ContractTestingNostromoAuctionFromScratch::takeoverCoordinatorWallet()) - coordinatorBefore, - expectedTakeoverCoordinatorGain(fees, cases[caseIndex].grossAmount)); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendRetention(fees, cases[caseIndex].grossAmount)); + for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++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}; + NOST::AuctionRevenueBreakdown expectedRevenue{}; + nostromo.calculateAuctionRevenueBreakdown(cases[caseIndex].grossAmount, expectedRevenue); + 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); + + 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, 1)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, cases[caseIndex].grossAmount, cases[caseIndex].grossAmount).errorCode, + static_cast(NOST::EAuctionError::Success)); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + EXPECT_EQ(nostromo.getAuctionShareholderFeeBasisPoints(cases[caseIndex].grossAmount), cases[caseIndex].expectedShareholderFeeBp); + EXPECT_EQ(getBalance(seller) - sellerBefore, expectedRevenue.sellerPayout); + + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, + cases[caseIndex].grossAmount - expectedRevenue.sellerPayout); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); + } + else + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedRevenue.managementFeeAmount); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedRevenue.developmentFeeAmount); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, + expectedRevenue.takeoverCoordinatorFeeAmount); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedRevenue.shareholderDividendAmount); + } + } } } From 2cabf04ef49fc7e2c3da8a5fc58115bc6845b6df Mon Sep 17 00:00:00 2001 From: N-010 Date: Sat, 18 Apr 2026 00:36:56 +0300 Subject: [PATCH 27/59] Synchronize auction pause states with `SyncAuctionPauseState`: ensure accurate timer adjustments, align deadlines with current pause state, and refactor balance-checking logic in tests to improve test accuracy. --- src/contracts/Nostromo.h | 58 +++++++++++++++++++++++++++++-- test/contract_nostromo.cpp | 70 +++++++++++++++++++++++--------------- 2 files changed, 97 insertions(+), 31 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index cc89beaad..6bd3b3d5b 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -1326,6 +1326,22 @@ struct NOST : public ContractBase } 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(); + } } END_EPOCH() @@ -1468,13 +1484,49 @@ struct NOST : public ContractBase PRIVATE_PROCEDURE_WITH_LOCALS(SyncAuctionPauseState) { locals.currentDate = qpi.now(); - if (state.get().isPostBeginEpochPauseArmed && (qpi.tick() - qpi.initialTick()) >= NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS) + CALL(GetAuctionPauseState, locals.getAuctionPauseStateInput, locals.getAuctionPauseStateOutput); + + 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; } - CALL(GetAuctionPauseState, locals.getAuctionPauseStateInput, locals.getAuctionPauseStateOutput); - if (locals.getAuctionPauseStateOutput.isPaused) { if (!state.get().isAuctionTimerPaused) diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index b6eb08e11..42a4c5c73 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -83,6 +83,7 @@ class ContractTestingNOST : protected ContractTesting void beginEpoch() { + system.initialTick = system.tick; ++system.epoch; callSystemProcedure(NOST_CONTRACT_INDEX, BEGIN_EPOCH); } @@ -1025,11 +1026,11 @@ TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetB, 1), 1); auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeTwoAssetLot(assetA, 2, assetB, 1), 100, 150, 10, 180); - const sint64 sellerBalanceBefore = getBalance(seller); NOST::AuctionRevenueBreakdown expectedRevenue{}; nostromo.calculateAuctionRevenueBreakdown(180ULL, expectedRevenue); const auto createOutput = nostromo.createAuction(seller, input); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + const sint64 sellerBalanceBefore = getBalance(seller); const auto bidOutput = nostromo.placeBid(bidder, createOutput.auctionId, 1, 180, 180); ASSERT_EQ(bidOutput.errorCode, static_cast(NOST::EAuctionError::Success)); @@ -1215,6 +1216,7 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY) * 1000ULL); auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto originalSellerDecisionDeadline = auction.sellerDecisionDeadline; ASSERT_EQ(auction.status, NOST::EAuctionStatus::PendingSellerDecision); EXPECT_EQ(auction.sellerDecisionDeadline.getHour(), 9); EXPECT_EQ(auction.sellerDecisionDeadline.getMinute(), 0); @@ -1222,26 +1224,38 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) nostromo.setNow(2026, 1, 9, 8, 59, 50); nostromo.beginEpoch(); - EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); + const uint32 launchPauseTicksAfterBeginEpoch = nostromo.getTicksBeforeAuctionLaunch().ticks; + EXPECT_EQ(launchPauseTicksAfterBeginEpoch, NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); nostromo.advanceAndEndTick(1000); - EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS - 1); + EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, launchPauseTicksAfterBeginEpoch - 1); nostromo.setNow(2026, 1, 9, 9, 8, 10); nostromo.advanceAndEndTick(0); auction = nostromo.getAuction(createOutput.auctionId).auction; ASSERT_EQ(auction.status, NOST::EAuctionStatus::PendingSellerDecision); - EXPECT_EQ(auction.sellerDecisionDeadline.getHour(), 9); - EXPECT_EQ(auction.sellerDecisionDeadline.getMinute(), 8); - EXPECT_EQ(auction.sellerDecisionDeadline.getSecond(), 21); + EXPECT_EQ(auction.sellerDecisionDeadline, originalSellerDecisionDeadline); - nostromo.setNow(2026, 1, 9, 9, 8, 20); + nostromo.advanceTicks(launchPauseTicksAfterBeginEpoch - 2); + auction = nostromo.getAuction(createOutput.auctionId).auction; + ASSERT_EQ(auction.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, 0U); + EXPECT_GT(auction.sellerDecisionDeadline, originalSellerDecisionDeadline); + + auto shiftedDeadline = auction.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.auctionId).auction.status, NOST::EAuctionStatus::PendingSellerDecision); - nostromo.setNow(2026, 1, 9, 9, 8, 22); + shiftedDeadline = auction.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.auctionId).auction.status, NOST::EAuctionStatus::Finalized); + auction = nostromo.getAuction(createOutput.auctionId).auction; + EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); } @@ -1263,15 +1277,15 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) NOST::AuctionRevenueBreakdown expectedRevenue{}; nostromo.calculateAuctionRevenueBreakdown(10000ULL, expectedRevenue); + + const auto createOutput = nostromo.createAuction( + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 10000, 10000, 10)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); 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); - - const auto createOutput = nostromo.createAuction( - seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 10000, 10000, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 10000, 10000).errorCode, static_cast(NOST::EAuctionError::Success)); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); @@ -1401,11 +1415,6 @@ TEST(ContractNostromoAuction, CancelAuctionRefundsAndChargesCorrectFeeAuction) const id bidderB(269 + routeIndex, 270 + routeIndex, 271 + routeIndex, 272 + routeIndex); const uint64 assetName = assetNameFromString(routeMode ? "CANBT1" : "CANBT0"); const Asset asset{seller, assetName}; - 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); nostromo.setRouteAllFeesToDevelopment(routeMode); EXPECT_EQ(nostromo.issueAsset(seller, assetName, 10), 10); @@ -1413,6 +1422,11 @@ TEST(ContractNostromoAuction, CancelAuctionRefundsAndChargesCorrectFeeAuction) const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + 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(bidderA, createOutput.auctionId, 2, 20, 40).errorCode, static_cast(NOST::EAuctionError::Success)); ASSERT_EQ(nostromo.placeBid(bidderB, createOutput.auctionId, 3, 15, 45).errorCode, static_cast(NOST::EAuctionError::Success)); @@ -1436,11 +1450,6 @@ TEST(ContractNostromoAuction, CancelAuctionRefundsAndChargesCorrectFeeAuction) const id bidder(277 + routeIndex, 278 + routeIndex, 279 + routeIndex, 280 + routeIndex); const uint64 assetName = assetNameFromString(routeMode ? "CANST1" : "CANST0"); const Asset asset{seller, assetName}; - 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); nostromo.setRouteAllFeesToDevelopment(routeMode); EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); @@ -1449,6 +1458,11 @@ TEST(ContractNostromoAuction, CancelAuctionRefundsAndChargesCorrectFeeAuction) const auto createOutput = nostromo.createAuction( seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + 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.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionId, 15); @@ -1610,11 +1624,6 @@ TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) const Asset asset{seller, cases[caseIndex].assetName}; NOST::AuctionRevenueBreakdown expectedRevenue{}; nostromo.calculateAuctionRevenueBreakdown(cases[caseIndex].grossAmount, expectedRevenue); - 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); nostromo.setRouteAllFeesToDevelopment(routeMode); EXPECT_EQ(nostromo.issueAsset(seller, cases[caseIndex].assetName, 1), 1); @@ -1624,6 +1633,11 @@ TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), cases[caseIndex].grossAmount, cases[caseIndex].grossAmount, 1)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + 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.auctionId, 1, cases[caseIndex].grossAmount, cases[caseIndex].grossAmount).errorCode, static_cast(NOST::EAuctionError::Success)); From f939847e468dc86cc48a5fe6e198bf53c94a5d67 Mon Sep 17 00:00:00 2001 From: N-010 Date: Sat, 18 Apr 2026 00:41:41 +0300 Subject: [PATCH 28/59] Add `expectedDividendPoolIncrease` helper in tests: calculate expected shareholder dividend pool changes and replace direct comparisons with computed expectations. Update relevant assertions for accuracy. --- test/contract_nostromo.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index 42a4c5c73..1fb612ae9 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -400,6 +400,13 @@ class ContractTestingNOST : protected ContractTesting state()->calculateAuctionRevenueBreakdown(grossAmount, output); } + 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); + } + uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount) { return state()->getAuctionShareholderFeeBasisPoints(grossAmount); } static id managementWallet() @@ -1277,6 +1284,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) NOST::AuctionRevenueBreakdown expectedRevenue{}; nostromo.calculateAuctionRevenueBreakdown(10000ULL, expectedRevenue); + const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedRevenue.shareholderDividendAmount); const auto createOutput = nostromo.createAuction( seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 10000, 10000, 10)); @@ -1308,7 +1316,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedRevenue.managementFeeAmount); EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedRevenue.developmentFeeAmount); EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedRevenue.takeoverCoordinatorFeeAmount); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedRevenue.shareholderDividendAmount); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); } } } @@ -1624,6 +1632,7 @@ TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) const Asset asset{seller, cases[caseIndex].assetName}; NOST::AuctionRevenueBreakdown expectedRevenue{}; nostromo.calculateAuctionRevenueBreakdown(cases[caseIndex].grossAmount, expectedRevenue); + const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedRevenue.shareholderDividendAmount); nostromo.setRouteAllFeesToDevelopment(routeMode); EXPECT_EQ(nostromo.issueAsset(seller, cases[caseIndex].assetName, 1), 1); @@ -1660,7 +1669,7 @@ TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedRevenue.developmentFeeAmount); EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedRevenue.takeoverCoordinatorFeeAmount); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedRevenue.shareholderDividendAmount); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); } } } From 20eddb6ddb6ca2925faf0886059365e805572b15 Mon Sep 17 00:00:00 2001 From: N-010 Date: Sat, 18 Apr 2026 00:59:06 +0300 Subject: [PATCH 29/59] Refactor `GetAuction` output structure: replace `AuctionData` with `Array` to support consistent handling of auction data, update all related method calls and tests. --- src/contracts/Nostromo.h | 10 +++---- test/contract_nostromo.cpp | 58 +++++++++++++++++++------------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 6bd3b3d5b..7e9843c68 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -505,7 +505,7 @@ struct NOST : public ContractBase struct GetAuction_output { /** @brief Persistent auction data stored for the requested auction. */ - AuctionData auction; + Array auction; }; /** @brief Input payload used to fetch one participant record from an auction. */ @@ -2946,7 +2946,7 @@ struct NOST : public ContractBase * @brief Returns the stored state of one auction. * @note The response contains the full persistent `AuctionData` record for the requested auction identifier. */ - PUBLIC_FUNCTION(GetAuction) { state.get().auctionList.get(input.auctionId, output.auction); } + PUBLIC_FUNCTION(GetAuction) { state.get().auctionList.get(input.auctionId, *reinterpret_cast(&output.auction)); } /** * @brief Returns the stored bid state of one wallet in one auction. @@ -3159,7 +3159,8 @@ struct NOST : public ContractBase * @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) + static void calculateAuctionRevenueBreakdown(uint64 grossAmount, const ContractState& state, + AuctionRevenueBreakdown& output) { output.sellerPayout = grossAmount; output.shareholderFeeBasisPoints = getAuctionShareholderFeeBasisPoints(grossAmount, state); @@ -3168,8 +3169,7 @@ struct NOST : public ContractBase output.managementFeeAmount = div(smul(grossAmount, state.get().managementFeeBasisPoints), 10000ULL); output.developmentFeeAmount = div(smul(grossAmount, state.get().developmentFeeBasisPoints), 10000ULL); output.takeoverCoordinatorBaseAmount = div(smul(grossAmount, state.get().takeoverCoordinatorFeeBasisPoints), 10000ULL); - output.takeoverCoordinatorFeeAmount = - output.takeoverCoordinatorBaseAmount + (output.shareholderFeeAmount - output.shareholderDividendAmount); + output.takeoverCoordinatorFeeAmount = output.takeoverCoordinatorBaseAmount + (output.shareholderFeeAmount - output.shareholderDividendAmount); output.sellerPayout = grossAmount - output.shareholderFeeAmount - output.managementFeeAmount - output.developmentFeeAmount - output.takeoverCoordinatorBaseAmount; } diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index 1fb612ae9..7673b5406 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -455,7 +455,7 @@ TEST(ContractNostromoAuction, InitialStateAndGettersAuction) const auto participantOutput = nostromo.getParticipant(missingAuction, missingParticipant); const auto launchPause = nostromo.getTicksBeforeAuctionLaunch(); - EXPECT_TRUE(isZero(auctionOutput.auction.auctionId)); + EXPECT_TRUE(isZero(auctionOutput.auction.get(0).auctionId)); EXPECT_EQ(participantOutput.found, 0); EXPECT_EQ(launchPause.ticks, 0U); @@ -512,7 +512,7 @@ TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); EXPECT_FALSE(isZero(output.auctionId)); - const auto auction = nostromo.getAuction(output.auctionId).auction; + const auto auction = nostromo.getAuction(output.auctionId).auction.get(0); EXPECT_EQ(auction.auctionId, output.auctionId); EXPECT_EQ(auction.quantityForSale, 9ULL); EXPECT_EQ(auction.minimumPurchaseQuantity, 0ULL); @@ -548,7 +548,7 @@ TEST(ContractNostromoAuction, CreateStandardBundleAuctionEscrowsMixedLotAuction) const auto output = nostromo.createAuction(seller, input); ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); - const auto auction = nostromo.getAuction(output.auctionId).auction; + const auto auction = nostromo.getAuction(output.auctionId).auction.get(0); EXPECT_EQ(auction.quantityForSale, 1ULL); EXPECT_EQ(auction.minimumPurchaseQuantity, 1ULL); EXPECT_EQ(auction.initialPrice, 100ULL); @@ -582,7 +582,7 @@ TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); - const auto auction = nostromo.getAuction(output.auctionId).auction; + const auto auction = nostromo.getAuction(output.auctionId).auction.get(0); EXPECT_EQ(auction.visibility, NOST::EAuctionVisibility::Private); EXPECT_TRUE(auction.allowedBidderWallets.contains(allowedBidder)); EXPECT_EQ(auction.requiredAccessAssets.population(), 0U); @@ -608,7 +608,7 @@ TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); - const auto auction = nostromo.getAuction(output.auctionId).auction; + const auto auction = nostromo.getAuction(output.auctionId).auction.get(0); EXPECT_EQ(auction.visibility, NOST::EAuctionVisibility::Private); EXPECT_EQ(auction.allowedBidderWallets.population(), 0U); EXPECT_TRUE(auction.requiredAccessAssets.contains(gateAsset)); @@ -855,7 +855,7 @@ TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAucti ASSERT_EQ(bidA1.errorCode, static_cast(NOST::EAuctionError::Success)); ASSERT_EQ(bidB.errorCode, static_cast(NOST::EAuctionError::Success)); - auto auction = nostromo.getAuction(createOutput.auctionId).auction; + auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); EXPECT_EQ(auction.highestBidder, bidderA); EXPECT_EQ(auction.highestBidPrice, 20ULL); EXPECT_EQ(auction.highestBidAmount, 40ULL); @@ -865,7 +865,7 @@ TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAucti EXPECT_EQ(bidA2.escrowedAmount, 28ULL); EXPECT_EQ(bidA2.refundedAmount, 40ULL); - auction = nostromo.getAuction(createOutput.auctionId).auction; + auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); EXPECT_EQ(auction.highestBidder, bidderB); EXPECT_EQ(auction.highestBidPrice, 15ULL); EXPECT_EQ(auction.highestBidAmount, 45ULL); @@ -898,7 +898,7 @@ TEST(ContractNostromoAuction, PlaceBatchBidExtendsAuctionNearEndAuction) const auto bidOutput = nostromo.placeBid(bidder, createOutput.auctionId, 1, 15, 15); ASSERT_EQ(bidOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); EXPECT_EQ(auction.auctionDurationSeconds, NOST_SECONDS_PER_DAY + NOST_AUCTION_EXTENSION_SECONDS); } @@ -1042,7 +1042,7 @@ TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) const auto bidOutput = nostromo.placeBid(bidder, createOutput.auctionId, 1, 180, 180); ASSERT_EQ(bidOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); const auto participant = nostromo.getParticipant(createOutput.auctionId, bidder); EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(auction.allocatedQuantity, 1ULL); @@ -1080,7 +1080,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); const auto participantA = nostromo.getParticipant(createOutput.auctionId, bidderA); const auto participantB = nostromo.getParticipant(createOutput.auctionId, bidderB); const auto participantC = nostromo.getParticipant(createOutput.auctionId, bidderC); @@ -1121,7 +1121,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(auction.allocatedQuantity, 2ULL); EXPECT_EQ(nostromo.managedShares(asset, bidder), 2); @@ -1145,7 +1145,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(auction.allocatedQuantity, 0ULL); EXPECT_TRUE(isZero(auction.highestBidder)); @@ -1169,15 +1169,15 @@ TEST(ContractNostromoAuction, WeeklyPauseShiftsActiveAuctionDeadlineAuction) nostromo.setNow(2026, 1, 7, 11, 40, 0); nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Active); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::Active); nostromo.setNow(2026, 1, 7, 12, 0, 0); nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Active); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::Active); nostromo.setNow(2026, 1, 7, 12, 10, 1); nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(nostromo.managedShares(asset, seller), 1); } @@ -1199,7 +1199,7 @@ TEST(ContractNostromoAuction, EndTickSkipsAuctionProcessingAtBootstrapTimeAuctio nostromo.setNow(2022, 4, 13, 12, 0, 0); nostromo.advanceAndEndTick(0); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); EXPECT_EQ(auction.status, NOST::EAuctionStatus::Active); EXPECT_EQ(auction.allocatedQuantity, 0ULL); EXPECT_EQ(nostromo.managedShares(asset, seller), 0); @@ -1222,7 +1222,7 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY) * 1000ULL); - auto auction = nostromo.getAuction(createOutput.auctionId).auction; + auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); const auto originalSellerDecisionDeadline = auction.sellerDecisionDeadline; ASSERT_EQ(auction.status, NOST::EAuctionStatus::PendingSellerDecision); EXPECT_EQ(auction.sellerDecisionDeadline.getHour(), 9); @@ -1239,12 +1239,12 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) nostromo.setNow(2026, 1, 9, 9, 8, 10); nostromo.advanceAndEndTick(0); - auction = nostromo.getAuction(createOutput.auctionId).auction; + auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); ASSERT_EQ(auction.status, NOST::EAuctionStatus::PendingSellerDecision); EXPECT_EQ(auction.sellerDecisionDeadline, originalSellerDecisionDeadline); nostromo.advanceTicks(launchPauseTicksAfterBeginEpoch - 2); - auction = nostromo.getAuction(createOutput.auctionId).auction; + auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); ASSERT_EQ(auction.status, NOST::EAuctionStatus::PendingSellerDecision); EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, 0U); EXPECT_GT(auction.sellerDecisionDeadline, originalSellerDecisionDeadline); @@ -1254,14 +1254,14 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) nostromo.setNow(shiftedDeadline.getYear(), shiftedDeadline.getMonth(), shiftedDeadline.getDay(), shiftedDeadline.getHour(), shiftedDeadline.getMinute(), shiftedDeadline.getSecond()); nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::PendingSellerDecision); shiftedDeadline = auction.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.auctionId).auction; + auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); } @@ -1298,7 +1298,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(auction.allocatedQuantity, 1ULL); EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); @@ -1339,14 +1339,14 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::PendingSellerDecision); const auto forbidden = nostromo.resolvePendingStandardAuction(id(999, 999, 999, 999), createOutput.auctionId, true); EXPECT_EQ(forbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); const auto acceptOutput = nostromo.resolvePendingStandardAuction(seller, createOutput.auctionId, true); EXPECT_EQ(acceptOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); } @@ -1370,7 +1370,7 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction EXPECT_EQ(rejectOutput.errorCode, static_cast(NOST::EAuctionError::Success)); EXPECT_EQ(rejectOutput.refundedAmount, 120ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); const auto participant = nostromo.getParticipant(createOutput.auctionId, bidder); EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(auction.allocatedQuantity, 0ULL); @@ -1397,10 +1397,10 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::PendingSellerDecision); nostromo.advanceAndEndTick((NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); const auto participant = nostromo.getParticipant(createOutput.auctionId, bidder); EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(auction.allocatedQuantity, 1ULL); @@ -1442,7 +1442,7 @@ TEST(ContractNostromoAuction, CancelAuctionRefundsAndChargesCorrectFeeAuction) EXPECT_EQ(cancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); EXPECT_EQ(cancelOutput.refundedAmount, 85ULL); EXPECT_EQ(cancelOutput.cancellationFee, 10ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Cancelled); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::Cancelled); EXPECT_EQ(nostromo.managedShares(asset, seller), 10); EXPECT_EQ(getBalance(seller) - sellerBefore, 0); EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); @@ -1477,7 +1477,7 @@ TEST(ContractNostromoAuction, CancelAuctionRefundsAndChargesCorrectFeeAuction) EXPECT_EQ(cancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); EXPECT_EQ(cancelOutput.refundedAmount, 120ULL); EXPECT_EQ(cancelOutput.cancellationFee, 15ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Cancelled); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::Cancelled); EXPECT_EQ(nostromo.managedShares(asset, seller), 1); EXPECT_EQ(getBalance(seller) - sellerBefore, 0); EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); From fba506ebdaee8eee105e97f6671a8512813e0aa1 Mon Sep 17 00:00:00 2001 From: N-010 Date: Sat, 18 Apr 2026 01:43:29 +0300 Subject: [PATCH 30/59] Simplify `GetAuction` output: replace `Array` with `AuctionData`, update method calls and tests accordingly. --- src/contracts/Nostromo.h | 8 +++--- test/contract_nostromo.cpp | 58 +++++++++++++++++++------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 7e9843c68..09c5b8708 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -505,7 +505,7 @@ struct NOST : public ContractBase struct GetAuction_output { /** @brief Persistent auction data stored for the requested auction. */ - Array auction; + AuctionData auction; }; /** @brief Input payload used to fetch one participant record from an auction. */ @@ -1961,7 +1961,7 @@ struct NOST : public ContractBase locals.hasPayloadCharacters = 0; locals.reachedTerminator = 0; - if (input.metadataIpfsCid.get(0) != 'b') + if (input.metadataIpfsCid.get(0) != QPI::Ch::b) { return; } @@ -1980,7 +1980,7 @@ struct NOST : public ContractBase return; } - if ((locals.cidChar >= 'a' && locals.cidChar <= 'z') || (locals.cidChar >= '2' && locals.cidChar <= '7')) + 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; @@ -2946,7 +2946,7 @@ struct NOST : public ContractBase * @brief Returns the stored state of one auction. * @note The response contains the full persistent `AuctionData` record for the requested auction identifier. */ - PUBLIC_FUNCTION(GetAuction) { state.get().auctionList.get(input.auctionId, *reinterpret_cast(&output.auction)); } + PUBLIC_FUNCTION(GetAuction) { state.get().auctionList.get(input.auctionId, output.auction); } /** * @brief Returns the stored bid state of one wallet in one auction. diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index 7673b5406..1fb612ae9 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -455,7 +455,7 @@ TEST(ContractNostromoAuction, InitialStateAndGettersAuction) const auto participantOutput = nostromo.getParticipant(missingAuction, missingParticipant); const auto launchPause = nostromo.getTicksBeforeAuctionLaunch(); - EXPECT_TRUE(isZero(auctionOutput.auction.get(0).auctionId)); + EXPECT_TRUE(isZero(auctionOutput.auction.auctionId)); EXPECT_EQ(participantOutput.found, 0); EXPECT_EQ(launchPause.ticks, 0U); @@ -512,7 +512,7 @@ TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); EXPECT_FALSE(isZero(output.auctionId)); - const auto auction = nostromo.getAuction(output.auctionId).auction.get(0); + const auto auction = nostromo.getAuction(output.auctionId).auction; EXPECT_EQ(auction.auctionId, output.auctionId); EXPECT_EQ(auction.quantityForSale, 9ULL); EXPECT_EQ(auction.minimumPurchaseQuantity, 0ULL); @@ -548,7 +548,7 @@ TEST(ContractNostromoAuction, CreateStandardBundleAuctionEscrowsMixedLotAuction) const auto output = nostromo.createAuction(seller, input); ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); - const auto auction = nostromo.getAuction(output.auctionId).auction.get(0); + const auto auction = nostromo.getAuction(output.auctionId).auction; EXPECT_EQ(auction.quantityForSale, 1ULL); EXPECT_EQ(auction.minimumPurchaseQuantity, 1ULL); EXPECT_EQ(auction.initialPrice, 100ULL); @@ -582,7 +582,7 @@ TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); - const auto auction = nostromo.getAuction(output.auctionId).auction.get(0); + const auto auction = nostromo.getAuction(output.auctionId).auction; EXPECT_EQ(auction.visibility, NOST::EAuctionVisibility::Private); EXPECT_TRUE(auction.allowedBidderWallets.contains(allowedBidder)); EXPECT_EQ(auction.requiredAccessAssets.population(), 0U); @@ -608,7 +608,7 @@ TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); - const auto auction = nostromo.getAuction(output.auctionId).auction.get(0); + const auto auction = nostromo.getAuction(output.auctionId).auction; EXPECT_EQ(auction.visibility, NOST::EAuctionVisibility::Private); EXPECT_EQ(auction.allowedBidderWallets.population(), 0U); EXPECT_TRUE(auction.requiredAccessAssets.contains(gateAsset)); @@ -855,7 +855,7 @@ TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAucti ASSERT_EQ(bidA1.errorCode, static_cast(NOST::EAuctionError::Success)); ASSERT_EQ(bidB.errorCode, static_cast(NOST::EAuctionError::Success)); - auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); + auto auction = nostromo.getAuction(createOutput.auctionId).auction; EXPECT_EQ(auction.highestBidder, bidderA); EXPECT_EQ(auction.highestBidPrice, 20ULL); EXPECT_EQ(auction.highestBidAmount, 40ULL); @@ -865,7 +865,7 @@ TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAucti EXPECT_EQ(bidA2.escrowedAmount, 28ULL); EXPECT_EQ(bidA2.refundedAmount, 40ULL); - auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); + auction = nostromo.getAuction(createOutput.auctionId).auction; EXPECT_EQ(auction.highestBidder, bidderB); EXPECT_EQ(auction.highestBidPrice, 15ULL); EXPECT_EQ(auction.highestBidAmount, 45ULL); @@ -898,7 +898,7 @@ TEST(ContractNostromoAuction, PlaceBatchBidExtendsAuctionNearEndAuction) const auto bidOutput = nostromo.placeBid(bidder, createOutput.auctionId, 1, 15, 15); ASSERT_EQ(bidOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; EXPECT_EQ(auction.auctionDurationSeconds, NOST_SECONDS_PER_DAY + NOST_AUCTION_EXTENSION_SECONDS); } @@ -1042,7 +1042,7 @@ TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) const auto bidOutput = nostromo.placeBid(bidder, createOutput.auctionId, 1, 180, 180); ASSERT_EQ(bidOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; const auto participant = nostromo.getParticipant(createOutput.auctionId, bidder); EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(auction.allocatedQuantity, 1ULL); @@ -1080,7 +1080,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; const auto participantA = nostromo.getParticipant(createOutput.auctionId, bidderA); const auto participantB = nostromo.getParticipant(createOutput.auctionId, bidderB); const auto participantC = nostromo.getParticipant(createOutput.auctionId, bidderC); @@ -1121,7 +1121,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(auction.allocatedQuantity, 2ULL); EXPECT_EQ(nostromo.managedShares(asset, bidder), 2); @@ -1145,7 +1145,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(auction.allocatedQuantity, 0ULL); EXPECT_TRUE(isZero(auction.highestBidder)); @@ -1169,15 +1169,15 @@ TEST(ContractNostromoAuction, WeeklyPauseShiftsActiveAuctionDeadlineAuction) nostromo.setNow(2026, 1, 7, 11, 40, 0); nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::Active); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Active); nostromo.setNow(2026, 1, 7, 12, 0, 0); nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::Active); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Active); nostromo.setNow(2026, 1, 7, 12, 10, 1); nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(nostromo.managedShares(asset, seller), 1); } @@ -1199,7 +1199,7 @@ TEST(ContractNostromoAuction, EndTickSkipsAuctionProcessingAtBootstrapTimeAuctio nostromo.setNow(2022, 4, 13, 12, 0, 0); nostromo.advanceAndEndTick(0); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; EXPECT_EQ(auction.status, NOST::EAuctionStatus::Active); EXPECT_EQ(auction.allocatedQuantity, 0ULL); EXPECT_EQ(nostromo.managedShares(asset, seller), 0); @@ -1222,7 +1222,7 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY) * 1000ULL); - auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); + auto auction = nostromo.getAuction(createOutput.auctionId).auction; const auto originalSellerDecisionDeadline = auction.sellerDecisionDeadline; ASSERT_EQ(auction.status, NOST::EAuctionStatus::PendingSellerDecision); EXPECT_EQ(auction.sellerDecisionDeadline.getHour(), 9); @@ -1239,12 +1239,12 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) nostromo.setNow(2026, 1, 9, 9, 8, 10); nostromo.advanceAndEndTick(0); - auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); + auction = nostromo.getAuction(createOutput.auctionId).auction; ASSERT_EQ(auction.status, NOST::EAuctionStatus::PendingSellerDecision); EXPECT_EQ(auction.sellerDecisionDeadline, originalSellerDecisionDeadline); nostromo.advanceTicks(launchPauseTicksAfterBeginEpoch - 2); - auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); + auction = nostromo.getAuction(createOutput.auctionId).auction; ASSERT_EQ(auction.status, NOST::EAuctionStatus::PendingSellerDecision); EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, 0U); EXPECT_GT(auction.sellerDecisionDeadline, originalSellerDecisionDeadline); @@ -1254,14 +1254,14 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) nostromo.setNow(shiftedDeadline.getYear(), shiftedDeadline.getMonth(), shiftedDeadline.getDay(), shiftedDeadline.getHour(), shiftedDeadline.getMinute(), shiftedDeadline.getSecond()); nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::PendingSellerDecision); shiftedDeadline = auction.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.auctionId).auction.get(0); + auction = nostromo.getAuction(createOutput.auctionId).auction; EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); } @@ -1298,7 +1298,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(auction.allocatedQuantity, 1ULL); EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); @@ -1339,14 +1339,14 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::PendingSellerDecision); const auto forbidden = nostromo.resolvePendingStandardAuction(id(999, 999, 999, 999), createOutput.auctionId, true); EXPECT_EQ(forbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); const auto acceptOutput = nostromo.resolvePendingStandardAuction(seller, createOutput.auctionId, true); EXPECT_EQ(acceptOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); } @@ -1370,7 +1370,7 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction EXPECT_EQ(rejectOutput.errorCode, static_cast(NOST::EAuctionError::Success)); EXPECT_EQ(rejectOutput.refundedAmount, 120ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; const auto participant = nostromo.getParticipant(createOutput.auctionId, bidder); EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(auction.allocatedQuantity, 0ULL); @@ -1397,10 +1397,10 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::PendingSellerDecision); nostromo.advanceAndEndTick((NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction.get(0); + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; const auto participant = nostromo.getParticipant(createOutput.auctionId, bidder); EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(auction.allocatedQuantity, 1ULL); @@ -1442,7 +1442,7 @@ TEST(ContractNostromoAuction, CancelAuctionRefundsAndChargesCorrectFeeAuction) EXPECT_EQ(cancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); EXPECT_EQ(cancelOutput.refundedAmount, 85ULL); EXPECT_EQ(cancelOutput.cancellationFee, 10ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::Cancelled); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Cancelled); EXPECT_EQ(nostromo.managedShares(asset, seller), 10); EXPECT_EQ(getBalance(seller) - sellerBefore, 0); EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); @@ -1477,7 +1477,7 @@ TEST(ContractNostromoAuction, CancelAuctionRefundsAndChargesCorrectFeeAuction) EXPECT_EQ(cancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); EXPECT_EQ(cancelOutput.refundedAmount, 120ULL); EXPECT_EQ(cancelOutput.cancellationFee, 15ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.get(0).status, NOST::EAuctionStatus::Cancelled); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Cancelled); EXPECT_EQ(nostromo.managedShares(asset, seller), 1); EXPECT_EQ(getBalance(seller) - sellerBefore, 0); EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); From b5bd915e905107f1c5875ef69a666d64227640b2 Mon Sep 17 00:00:00 2001 From: N-010 Date: Tue, 21 Apr 2026 21:27:19 +0300 Subject: [PATCH 31/59] Introduce auction service fee breakdown: add calculation, distribution logic, and integrate with private auction and cancellation flows. Update related tests and constants. --- src/contracts/Nostromo.h | 140 +++++++++++++++++++++++++++++++++++-- test/contract_nostromo.cpp | 135 +++++++++++++++++++++++++++-------- 2 files changed, 240 insertions(+), 35 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 09c5b8708..080d09452 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -21,6 +21,10 @@ constexpr uint64 NOST_DEFAULT_AUCTION_CANCELLATION_FEE_BP = 1000ULL; constexpr uint64 NOST_DEFAULT_AUCTION_MANAGEMENT_FEE_BP = 50ULL; constexpr uint64 NOST_DEFAULT_AUCTION_DEVELOPMENT_FEE_BP = 50ULL; constexpr uint64 NOST_DEFAULT_AUCTION_TAKEOVER_COORDINATOR_FEE_BP = 50ULL; +constexpr uint64 NOST_AUCTION_SERVICE_FEE_SHAREHOLDER_BP = 7270ULL; +constexpr uint64 NOST_AUCTION_SERVICE_FEE_MANAGEMENT_BP = 910ULL; +constexpr uint64 NOST_AUCTION_SERVICE_FEE_DEVELOPMENT_BP = 910ULL; +constexpr uint64 NOST_AUCTION_SERVICE_FEE_TAKEOVER_COORDINATOR_BP = 910ULL; constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_DIVIDEND_BP = 9000ULL; constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_1 = 500ULL; constexpr uint64 NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_2 = 450ULL; @@ -226,7 +230,7 @@ struct NOST : public ContractBase /** @brief Configured cancellation fee rate in basis points. */ uint64 auctionCancellationFeeBasisPoints; - /** @brief Undistributed auction shareholder revenue reserved for contract dividends. */ + /** @brief Undistributed shareholder revenue from auction proceeds and service fees reserved for contract dividends. */ uint64 auctionShareholderDividendPool; /** @brief Configured management fee rate in basis points, charged from auction proceeds. */ @@ -605,6 +609,25 @@ struct NOST : public ContractBase 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; @@ -853,6 +876,20 @@ struct NOST : public ContractBase uint8 success; }; + /** @brief Internal input used to split private-auction and cancellation service fees between shareholders and configured recipients. */ + struct DistributeAuctionServiceFee_input + { + /** @brief Fee amount that should be distributed. */ + uint64 feeAmount; + }; + + /** @brief Internal output returned after service-fee distribution is completed. */ + struct DistributeAuctionServiceFee_output + { + /** @brief Flag indicating whether the service-fee distribution completed successfully. */ + uint8 success; + }; + /** @brief Internal input used to compute the remaining post-BEGIN_EPOCH launch pause. */ struct GetTicksBeforeAuctionLaunchInternal_input { @@ -1121,6 +1158,13 @@ struct NOST : public ContractBase uint64 dividendPerShare; }; + struct DistributeAuctionServiceFee_locals + { + AuctionServiceFeeBreakdown auctionServiceFeeBreakdown; + uint64 distributedDividendAmount; + uint64 dividendPerShare; + }; + struct RejectStandardAuction_locals { AuctionData auction; @@ -1147,6 +1191,7 @@ struct NOST : public ContractBase VerifyAuctionLotBalances_input verifyAuctionLotBalancesInput; EscrowAuctionLotAssets_input escrowAuctionLotAssetsInput; RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; + DistributeAuctionServiceFee_input distributeAuctionServiceFeeInput; sint64 requiredFee; uint64 resolvedQuantityForSale; uint64 resolvedMinimumPurchaseQuantity; @@ -1155,6 +1200,7 @@ struct NOST : public ContractBase RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; EscrowAuctionLotAssets_output escrowAuctionLotAssetsOutput; VerifyAuctionLotBalances_output verifyAuctionLotBalancesOutput; + DistributeAuctionServiceFee_output distributeAuctionServiceFeeOutput; }; struct PlaceBid_locals @@ -1180,6 +1226,8 @@ struct NOST : public ContractBase AuctionParticipantKey participantKey; RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; + DistributeAuctionServiceFee_input distributeAuctionServiceFeeInput; + DistributeAuctionServiceFee_output distributeAuctionServiceFeeOutput; DateAndTime currentDate; uint64 cancellationBaseAmount; sint64 participantIndex; @@ -1651,6 +1699,49 @@ struct NOST : public ContractBase output.success = 1; } + PRIVATE_PROCEDURE_WITH_LOCALS(DistributeAuctionServiceFee) + { + output.success = 0; + + if (input.feeAmount == 0) + { + output.success = 1; + return; + } + + if (routeAllFeesToDevelopment(state)) + { + qpi.transfer(state.get().development, input.feeAmount); + output.success = 1; + return; + } + + calculateAuctionServiceFeeBreakdown(input.feeAmount, locals.auctionServiceFeeBreakdown); + state.mut().auctionShareholderDividendPool = + sadd(state.get().auctionShareholderDividendPool, locals.auctionServiceFeeBreakdown.shareholderDividendAmount); + if (locals.auctionServiceFeeBreakdown.managementFeeAmount > 0) + { + qpi.transfer(state.get().management, locals.auctionServiceFeeBreakdown.managementFeeAmount); + } + if (locals.auctionServiceFeeBreakdown.developmentFeeAmount > 0) + { + qpi.transfer(state.get().development, locals.auctionServiceFeeBreakdown.developmentFeeAmount); + } + if (locals.auctionServiceFeeBreakdown.takeoverCoordinatorFeeAmount > 0) + { + qpi.transfer(state.get().takeoverCoordinator, locals.auctionServiceFeeBreakdown.takeoverCoordinatorFeeAmount); + } + + 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; + } + + output.success = 1; + } + PRIVATE_FUNCTION_WITH_LOCALS(CountAllowedBidderWallets) { output.allowedWalletCount = 0; @@ -2324,7 +2415,8 @@ struct NOST : public ContractBase /** * @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 Private auctions require the configured private auction fee and must use exactly one access mode. + * @note Private auctions require the configured private auction fee, which is distributed between shareholders and the configured fee recipients, + * and must use exactly one access mode. */ PUBLIC_PROCEDURE_WITH_LOCALS(CreateAuction) { @@ -2526,6 +2618,9 @@ struct NOST : public ContractBase return; } + locals.distributeAuctionServiceFeeInput.feeAmount = static_cast(locals.requiredFee); + CALL(DistributeAuctionServiceFee, locals.distributeAuctionServiceFeeInput, locals.distributeAuctionServiceFeeOutput); + if (qpi.invocationReward() > locals.requiredFee) { qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.requiredFee); @@ -2672,11 +2767,15 @@ struct NOST : public ContractBase } /** - * @brief Cancels an active auction and refunds every escrowed bid. - * @note The cancellation fee is based on the current highest bid or on the configured reserve price for the full batch quantity or standard lot. + * @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) { + output.refundedAmount = 0; + output.cancellationFee = 0; output.errorCode = static_cast(EAuctionError::InvalidInput); if (!state.get().auctionList.get(input.auctionId, locals.auction)) @@ -2709,10 +2808,20 @@ struct NOST : public ContractBase return; } - locals.cancellationBaseAmount = max(locals.auction.highestBidAmount, locals.auction.salePrice); + if (locals.auction.highestBidAmount > 0) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = static_cast(EAuctionError::Forbidden); + return; + } + + locals.cancellationBaseAmount = locals.auction.salePrice; if (locals.auction.type == EAuctionType::Batch) { - locals.cancellationBaseAmount = max(locals.auction.highestBidAmount, smul(locals.auction.salePrice, locals.auction.quantityForSale)); + locals.cancellationBaseAmount = smul(locals.auction.salePrice, locals.auction.quantityForSale); } output.cancellationFee = div(smul(locals.cancellationBaseAmount, state.get().auctionCancellationFeeBasisPoints), 10000ULL); @@ -2755,6 +2864,9 @@ struct NOST : public ContractBase state.mut().auctionList.replace(input.auctionId, locals.auction); addClosedAuctionToHistory(state, locals.auction.auctionId); + locals.distributeAuctionServiceFeeInput.feeAmount = output.cancellationFee; + CALL(DistributeAuctionServiceFee, locals.distributeAuctionServiceFeeInput, locals.distributeAuctionServiceFeeOutput); + if (static_cast(qpi.invocationReward()) > output.cancellationFee) { qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - output.cancellationFee); @@ -3174,6 +3286,22 @@ struct NOST : public ContractBase output.takeoverCoordinatorBaseAmount; } + /** + * @brief Computes the exact service-fee split without performing transfers. + * @note Keep this helper pure so tests can reuse the same arithmetic as `DistributeAuctionServiceFee`. + */ + static void calculateAuctionServiceFeeBreakdown(uint64 feeAmount, AuctionServiceFeeBreakdown& output) + { + output.shareholderDividendAmount = div(smul(feeAmount, NOST_AUCTION_SERVICE_FEE_SHAREHOLDER_BP), 10000ULL); + output.managementFeeAmount = div(smul(feeAmount, NOST_AUCTION_SERVICE_FEE_MANAGEMENT_BP), 10000ULL); + output.developmentFeeAmount = div(smul(feeAmount, NOST_AUCTION_SERVICE_FEE_DEVELOPMENT_BP), 10000ULL); + output.takeoverCoordinatorFeeAmount = div(smul(feeAmount, NOST_AUCTION_SERVICE_FEE_TAKEOVER_COORDINATOR_BP), 10000ULL); + // Shareholders receive the rounding remainder so the entire collected fee is distributed on-chain. + output.shareholderDividendAmount = + output.shareholderDividendAmount + (feeAmount - output.shareholderDividendAmount - output.managementFeeAmount - + output.developmentFeeAmount - output.takeoverCoordinatorFeeAmount); + } + static sint64 getCreateAuctionFee(EAuctionVisibility visibility, const ContractState& state) { return visibility == EAuctionVisibility::Private ? state.get().privateAuctionFee : 0; diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index 1fb612ae9..607d30991 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -24,6 +24,11 @@ class NOSTChecker : public NOST, public NOST::StateData NOST::calculateAuctionRevenueBreakdown(grossAmount, asState(), output); } + void calculateAuctionServiceFeeBreakdown(uint64 feeAmount, AuctionServiceFeeBreakdown& output) const + { + NOST::calculateAuctionServiceFeeBreakdown(feeAmount, output); + } + uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount) const { return NOST::getAuctionShareholderFeeBasisPoints(grossAmount, asState()); } }; @@ -400,6 +405,11 @@ class ContractTestingNOST : protected ContractTesting state()->calculateAuctionRevenueBreakdown(grossAmount, output); } + void calculateAuctionServiceFeeBreakdown(uint64 feeAmount, NOST::AuctionServiceFeeBreakdown& output) + { + state()->calculateAuctionServiceFeeBreakdown(feeAmount, output); + } + sint64 expectedDividendPoolIncrease(uint64 addedDividendAmount) const { const uint64 poolBefore = stateData().auctionShareholderDividendPool; @@ -616,7 +626,7 @@ TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction } } -TEST(ContractNostromoAuction, PrivateAuctionFeeRemainsOnContractInBothModesAuction) +TEST(ContractNostromoAuction, PrivateAuctionFeeIsDistributedAcrossRecipientsAuction) { const uint8 routeModes[] = {0, 1}; for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) @@ -639,6 +649,9 @@ TEST(ContractNostromoAuction, PrivateAuctionFeeRemainsOnContractInBothModesAucti const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + NOST::AuctionServiceFeeBreakdown expectedBreakdown{}; + nostromo.calculateAuctionServiceFeeBreakdown(static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE), expectedBreakdown); + const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedBreakdown.shareholderDividendAmount); auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 4, 12); input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); @@ -648,10 +661,21 @@ TEST(ContractNostromoAuction, PrivateAuctionFeeRemainsOnContractInBothModesAucti ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); 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, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + if (routeMode != 0) + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); + } + else + { + EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedBreakdown.managementFeeAmount); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedBreakdown.developmentFeeAmount); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, + expectedBreakdown.takeoverCoordinatorFeeAmount); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); + } } } @@ -1410,7 +1434,7 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction } } -TEST(ContractNostromoAuction, CancelAuctionRefundsAndChargesCorrectFeeAuction) +TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) { const uint8 routeModes[] = {0, 1}; for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++routeIndex) @@ -1419,8 +1443,6 @@ TEST(ContractNostromoAuction, CancelAuctionRefundsAndChargesCorrectFeeAuction) ContractTestingNOST nostromo; const uint8 routeMode = routeModes[routeIndex]; const id seller(261 + routeIndex, 262 + routeIndex, 263 + routeIndex, 264 + routeIndex); - const id bidderA(265 + routeIndex, 266 + routeIndex, 267 + routeIndex, 268 + routeIndex); - const id bidderB(269 + routeIndex, 270 + routeIndex, 271 + routeIndex, 272 + routeIndex); const uint64 assetName = assetNameFromString(routeMode ? "CANBT1" : "CANBT0"); const Asset asset{seller, assetName}; @@ -1428,34 +1450,45 @@ TEST(ContractNostromoAuction, CancelAuctionRefundsAndChargesCorrectFeeAuction) 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, 10)); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 1000)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); 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(bidderA, createOutput.auctionId, 2, 20, 40).errorCode, static_cast(NOST::EAuctionError::Success)); - ASSERT_EQ(nostromo.placeBid(bidderB, createOutput.auctionId, 3, 15, 45).errorCode, static_cast(NOST::EAuctionError::Success)); + NOST::AuctionServiceFeeBreakdown expectedBreakdown{}; + nostromo.calculateAuctionServiceFeeBreakdown(1000ULL, expectedBreakdown); + const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedBreakdown.shareholderDividendAmount); - const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionId, 10); + const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionId, 1000); EXPECT_EQ(cancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - EXPECT_EQ(cancelOutput.refundedAmount, 85ULL); - EXPECT_EQ(cancelOutput.cancellationFee, 10ULL); + EXPECT_EQ(cancelOutput.refundedAmount, 0ULL); + EXPECT_EQ(cancelOutput.cancellationFee, 1000ULL); EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.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, 10); + 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, expectedBreakdown.managementFeeAmount); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedBreakdown.developmentFeeAmount); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, + expectedBreakdown.takeoverCoordinatorFeeAmount); + 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 id bidder(277 + routeIndex, 278 + routeIndex, 279 + routeIndex, 280 + routeIndex); const uint64 assetName = assetNameFromString(routeMode ? "CANST1" : "CANST0"); const Asset asset{seller, assetName}; @@ -1464,31 +1497,44 @@ TEST(ContractNostromoAuction, CancelAuctionRefundsAndChargesCorrectFeeAuction) EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); const auto createOutput = nostromo.createAuction( - seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 8000, 10000, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); 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.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); + NOST::AuctionServiceFeeBreakdown expectedBreakdown{}; + nostromo.calculateAuctionServiceFeeBreakdown(1000ULL, expectedBreakdown); + const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedBreakdown.shareholderDividendAmount); - const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionId, 15); + const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionId, 1000); EXPECT_EQ(cancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - EXPECT_EQ(cancelOutput.refundedAmount, 120ULL); - EXPECT_EQ(cancelOutput.cancellationFee, 15ULL); + EXPECT_EQ(cancelOutput.refundedAmount, 0ULL); + EXPECT_EQ(cancelOutput.cancellationFee, 1000ULL); EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.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, 15); + 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, expectedBreakdown.managementFeeAmount); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedBreakdown.developmentFeeAmount); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, + expectedBreakdown.takeoverCoordinatorFeeAmount); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedDividendPoolIncrease); + } } } } -TEST(ContractNostromoAuction, CancelAuctionRejectsInvalidCasesAuction) +TEST(ContractNostromoAuction, CancelAuctionRejectsAfterBidAuction) { ContractTestingNOST nostromo; const id seller(281, 282, 283, 284); @@ -1509,6 +1555,37 @@ TEST(ContractNostromoAuction, CancelAuctionRejectsInvalidCasesAuction) const auto forbidden = nostromo.cancelAuction(bidder, createOutput.auctionId, 10); EXPECT_EQ(forbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + const auto insufficient = nostromo.cancelAuction(seller, createOutput.auctionId, 1); + EXPECT_EQ(insufficient.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + + const auto success = nostromo.cancelAuction(seller, createOutput.auctionId, 2); + EXPECT_EQ(success.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + + const auto closed = nostromo.cancelAuction(seller, createOutput.auctionId, 2); + EXPECT_EQ(closed.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.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, static_cast(NOST::EAuctionError::Success)); + + const auto notFound = nostromo.cancelAuction(seller, id(801, 0, 0, 0), 10); + EXPECT_EQ(notFound.errorCode, static_cast(NOST::EAuctionError::AuctionNotFound)); + + const auto forbidden = nostromo.cancelAuction(outsider, createOutput.auctionId, 10); + EXPECT_EQ(forbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + const auto insufficient = nostromo.cancelAuction(seller, createOutput.auctionId, 1); EXPECT_EQ(insufficient.errorCode, static_cast(NOST::EAuctionError::InsufficientFunds)); From 2359300a69e34af3b6c5b68c8712e6a6255dd08e Mon Sep 17 00:00:00 2001 From: N-010 Date: Sun, 26 Apr 2026 00:04:03 +0300 Subject: [PATCH 32/59] Refactor `TransferShareManagementRights`: introduce locals structure, implement safer flow with validation checks, and ensure proper invocation reward handling and refund logic. --- src/contracts/Nostromo.h | 46 +++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 080d09452..878c9b456 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -1286,6 +1286,14 @@ struct NOST : public ContractBase sint64 transferredNumberOfShares; }; + struct TransferShareManagementRights_locals + { + sint64 result; + sint64 reward; + sint64 refundAmount; + bit success; + }; + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() { REGISTER_USER_PROCEDURE(CreateAuction, 1); @@ -3136,36 +3144,34 @@ struct NOST : public ContractBase * @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. */ - PUBLIC_PROCEDURE(TransferShareManagementRights) + PUBLIC_PROCEDURE_WITH_LOCALS(TransferShareManagementRights) { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } + locals.reward = qpi.invocationReward(); + locals.refundAmount = locals.reward; + locals.success = false; + output.transferredNumberOfShares = 0; - if (input.numberOfShares <= 0 || input.asset.assetName == 0 || input.newManagingContractIndex == 0) + if (input.numberOfShares > 0 && qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer, qpi.invocator(), qpi.invocator(), + SELF_INDEX, SELF_INDEX) >= input.numberOfShares) { - output.transferredNumberOfShares = 0; - return; + locals.result = qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, input.newManagingContractIndex, + input.newManagingContractIndex, locals.reward); + if (locals.result != INVALID_AMOUNT && locals.result >= 0) + { + locals.success = true; + locals.refundAmount = locals.reward - locals.result; + } } - if (qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < - input.numberOfShares) + if (locals.success) { - output.transferredNumberOfShares = 0; - return; + output.transferredNumberOfShares = input.numberOfShares; } - if (qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, input.newManagingContractIndex, - input.newManagingContractIndex, state.get().qxTransferFee) < 0) + if (locals.refundAmount > 0) { - // error - output.transferredNumberOfShares = 0; - return; + qpi.transfer(qpi.invocator(), locals.refundAmount); } - - // success - output.transferredNumberOfShares = input.numberOfShares; } protected: From 28abce34f02ed6c8c29e0814065022a1352259c6 Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 29 Apr 2026 20:49:34 +0300 Subject: [PATCH 33/59] Refactor auction data structures: introduce `AuctionCore` for shared fields, separate persistent state (`AuctionData`) and ABI views (`AuctionView`), and update all references and logic to use the new structure. --- src/contracts/Nostromo.h | 435 +++++++++++++++++++++++---------------- 1 file changed, 259 insertions(+), 176 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 878c9b456..de726659e 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -138,16 +138,40 @@ struct NOST : public ContractBase }; /** - * @brief Stores all persistent data for one auction. - * @note The same struct is shared by batch and standard auctions. + * @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 AuctionData + struct AuctionCore { + /** @brief Auction lot contents; one non-empty entry means a single asset, multiple non-empty entries mean a bundle. */ + Array auctionLotItems; + + /** @brief Lowercase base32 CIDv1 stored in Pinata for the auction name and description metadata. */ + Array metadataIpfsCid; + /** @brief Unique identifier of the auction created in the Auction House. */ id auctionId; + /** @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; @@ -182,44 +206,53 @@ struct NOST : public ContractBase /** @brief Auction duration in seconds, derived from the duration configured in days. */ uint64 auctionDurationSeconds; - /** @brief Timestamp when the seller created the auction. */ - DateAndTime createdAt; - - /** @brief Timestamp of the most recent accepted bid. */ - DateAndTime lastBidAt; + /** @brief Auction House mode: Batch Auction or Standard Auction. */ + EAuctionType type; - /** @brief Deadline for the seller to accept or reject a standard auction bid that ended between Initial Price and Sale Price. */ - DateAndTime sellerDecisionDeadline; + /** @brief Auction visibility: public or restricted private access. */ + EAuctionVisibility visibility; - /** @brief Timestamp when the auction was finalized, cancelled, or otherwise settled. */ - DateAndTime settledAt; + /** @brief Current lifecycle status of the auction, including the seller decision phase for standard auctions. */ + EAuctionStatus status; + }; - /** @brief Wallet that created the auction and offers the lot for sale. */ - id seller; + /** + * @brief Stores all persistent data for one auction. + * @note The same struct is shared by batch and standard auctions. + */ + struct AuctionData + { + /** @brief Fields shared with the public auction view. */ + AuctionCore core; - /** @brief Wallet that currently holds the highest bid. */ - id highestBidder; + /** @brief Wallet whitelist used when the private auction uses wallet-based access. */ + HashSet allowedBidderWallets; /** @brief Asset set required for participation when the private auction uses asset-based access. */ HashSet requiredAccessAssets; + }; - /** @brief Auction lot contents; one non-empty entry means a single asset, multiple non-empty entries mean a bundle. */ - Array auctionLotItems; - - /** @brief Wallet whitelist used when the private auction uses wallet-based access. */ - HashSet allowedBidderWallets; + /** + * @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 + { + /** @brief Fields shared with the persistent auction record. */ + AuctionCore core; - /** @brief Lowercase base32 CIDv1 stored in Pinata for the auction name and description metadata. */ - Array metadataIpfsCid; + /** @brief Wallet list used when the private auction restricts participation to predefined wallets. */ + Array allowedBidderWallets; - /** @brief Auction House mode: Batch Auction or Standard Auction. */ - EAuctionType type; + /** @brief Asset list required to participate when the private auction uses asset-based access. */ + Array requiredAccessAssets; - /** @brief Auction visibility: public or restricted private access. */ - EAuctionVisibility visibility; + /** @brief Number of populated entries in `requiredAccessAssets`. */ + uint64 requiredAccessAssetCount; - /** @brief Current lifecycle status of the auction, including the seller decision phase for standard auctions. */ - EAuctionStatus status; + /** @brief Number of populated entries in `allowedBidderWallets`. */ + uint64 allowedBidderWalletCount; }; struct StateData @@ -508,8 +541,11 @@ struct NOST : public ContractBase /** @brief Auction data returned by the read-only auction getter. */ struct GetAuction_output { - /** @brief Persistent auction data stored for the requested auction. */ - AuctionData auction; + /** @brief Serializable auction data stored for the requested auction. */ + AuctionView auction; + + /** @brief Flag indicating whether the auction record exists. */ + uint8 found; }; /** @brief Input payload used to fetch one participant record from an auction. */ @@ -732,11 +768,20 @@ struct NOST : public ContractBase uint64 requiredAccessAssetIndex; }; + struct GetAuction_locals + { + AuctionData auction; + Asset requiredAccessAsset; + id allowedBidderWallet; + sint64 requiredAccessAssetSetIndex; + sint64 allowedBidderWalletSetIndex; + }; + /** @brief Internal input used to verify whether the invocator owns at least one required private access asset. */ struct HasRequiredAccessAsset_input { - /** @brief Auction whose private asset-based access rules should be evaluated. */ - AuctionData auction; + /** @brief Identifier of the auction whose private asset-based access rules should be evaluated. */ + id auctionId; }; /** @brief Internal output of the private asset access check. */ @@ -748,6 +793,7 @@ struct NOST : public ContractBase struct HasRequiredAccessAsset_locals { + AuctionData auction; Asset requiredAccessAsset; sint64 requiredAccessAssetSetIndex; sint64 possessedAccessShares; @@ -1421,40 +1467,41 @@ struct NOST : public ContractBase while (locals.auctionIndex != NULL_INDEX) { locals.auction = state.get().auctionList.value(locals.auctionIndex); - if (locals.auction.status == EAuctionStatus::Active) + if (locals.auction.core.status == EAuctionStatus::Active) { - diffDateInSecond(locals.auction.createdAt, locals.currentDate, locals.elapsedSeconds); - if (locals.elapsedSeconds >= locals.auction.auctionDurationSeconds) + diffDateInSecond(locals.auction.core.createdAt, locals.currentDate, locals.elapsedSeconds); + if (locals.elapsedSeconds >= locals.auction.core.auctionDurationSeconds) { - switch (locals.auction.type) + switch (locals.auction.core.type) { case EAuctionType::Batch: - locals.finalizeBatchAuctionInput.auctionId = locals.auction.auctionId; + locals.finalizeBatchAuctionInput.auctionId = locals.auction.core.auctionId; locals.finalizeBatchAuctionInput.currentDate = locals.currentDate; CALL(FinalizeBatchAuction, locals.finalizeBatchAuctionInput, locals.finalizeBatchAuctionOutput); break; case EAuctionType::Standard: - if (locals.auction.highestBidAmount == 0 || locals.auction.highestBidPrice >= locals.auction.salePrice) + if (locals.auction.core.highestBidAmount == 0 || locals.auction.core.highestBidPrice >= locals.auction.core.salePrice) { - locals.finalizeStandardAuctionInput.auctionId = locals.auction.auctionId; + locals.finalizeStandardAuctionInput.auctionId = locals.auction.core.auctionId; locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); } else { - locals.auction.status = EAuctionStatus::PendingSellerDecision; - locals.auction.sellerDecisionDeadline = locals.currentDate; - locals.auction.sellerDecisionDeadline.add(0, 0, 0, 0, 0, NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS); - state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); + 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.auctionId, locals.auction); } break; default: break; }; } } - else if (locals.auction.status == EAuctionStatus::PendingSellerDecision && locals.auction.sellerDecisionDeadline <= locals.currentDate) + else if (locals.auction.core.status == EAuctionStatus::PendingSellerDecision && + locals.auction.core.sellerDecisionDeadline <= locals.currentDate) { - locals.finalizeStandardAuctionInput.auctionId = locals.auction.auctionId; + locals.finalizeStandardAuctionInput.auctionId = locals.auction.core.auctionId; locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); } @@ -1625,15 +1672,15 @@ struct NOST : public ContractBase while (locals.auctionIndex != NULL_INDEX) { locals.auction = state.get().auctionList.value(locals.auctionIndex); - if (locals.auction.status == EAuctionStatus::Active) + if (locals.auction.core.status == EAuctionStatus::Active) { - locals.auction.auctionDurationSeconds = sadd(locals.auction.auctionDurationSeconds, locals.pausedSeconds); - state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); + locals.auction.core.auctionDurationSeconds = sadd(locals.auction.core.auctionDurationSeconds, locals.pausedSeconds); + state.mut().auctionList.replace(locals.auction.core.auctionId, locals.auction); } - else if (locals.auction.status == EAuctionStatus::PendingSellerDecision && locals.auction.sellerDecisionDeadline.isValid()) + else if (locals.auction.core.status == EAuctionStatus::PendingSellerDecision && locals.auction.core.sellerDecisionDeadline.isValid()) { - locals.auction.sellerDecisionDeadline.add(0, 0, 0, 0, 0, static_cast(locals.pausedSeconds)); - state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); + locals.auction.core.sellerDecisionDeadline.add(0, 0, 0, 0, 0, static_cast(locals.pausedSeconds)); + state.mut().auctionList.replace(locals.auction.core.auctionId, locals.auction); } locals.auctionIndex = state.get().auctionList.nextElementIndex(locals.auctionIndex); } @@ -1779,11 +1826,16 @@ struct NOST : public ContractBase PRIVATE_FUNCTION_WITH_LOCALS(HasRequiredAccessAsset) { output.hasRequiredAccessAsset = 0; - for (locals.requiredAccessAssetSetIndex = input.auction.requiredAccessAssets.nextElementIndex(NULL_INDEX); + if (!state.get().auctionList.get(input.auctionId, locals.auction)) + { + return; + } + + for (locals.requiredAccessAssetSetIndex = locals.auction.requiredAccessAssets.nextElementIndex(NULL_INDEX); locals.requiredAccessAssetSetIndex != NULL_INDEX; - locals.requiredAccessAssetSetIndex = input.auction.requiredAccessAssets.nextElementIndex(locals.requiredAccessAssetSetIndex)) + locals.requiredAccessAssetSetIndex = locals.auction.requiredAccessAssets.nextElementIndex(locals.requiredAccessAssetSetIndex)) { - locals.requiredAccessAsset = input.auction.requiredAccessAssets.key(locals.requiredAccessAssetSetIndex); + locals.requiredAccessAsset = locals.auction.requiredAccessAssets.key(locals.requiredAccessAssetSetIndex); locals.possessedAccessShares = qpi.numberOfShares(locals.requiredAccessAsset, AssetOwnershipSelect::byOwner(qpi.invocator()), AssetPossessionSelect::byPossessor(qpi.invocator())); if (locals.possessedAccessShares > 0) @@ -1803,7 +1855,7 @@ struct NOST : public ContractBase return; } - if (locals.auction.type != EAuctionType::Batch) + if (locals.auction.core.type != EAuctionType::Batch) { return; } @@ -1835,20 +1887,20 @@ struct NOST : public ContractBase if (locals.bestParticipantFound) { - locals.auction.highestBidder = locals.bestParticipantKey.participant; - locals.auction.highestBidPrice = locals.bestParticipantData.bidAmount; - locals.auction.highestBidQuantity = locals.bestParticipantData.requestedQuantity; - locals.auction.highestBidAmount = locals.bestParticipantData.escrowedAmount; + locals.auction.core.highestBidder = locals.bestParticipantKey.participant; + locals.auction.core.highestBidPrice = locals.bestParticipantData.bidAmount; + locals.auction.core.highestBidQuantity = locals.bestParticipantData.requestedQuantity; + locals.auction.core.highestBidAmount = locals.bestParticipantData.escrowedAmount; } else { - locals.auction.highestBidAmount = 0; - locals.auction.highestBidPrice = 0; - locals.auction.highestBidQuantity = 0; - locals.auction.highestBidder = NULL_ID; + locals.auction.core.highestBidAmount = 0; + locals.auction.core.highestBidPrice = 0; + locals.auction.core.highestBidQuantity = 0; + locals.auction.core.highestBidder = NULL_ID; } - state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); + state.mut().auctionList.replace(locals.auction.core.auctionId, locals.auction); } PRIVATE_PROCEDURE_WITH_LOCALS(ProcessBatchBid) @@ -1869,7 +1921,7 @@ struct NOST : public ContractBase return; } - if (input.bidAmount < locals.auction.salePrice) + if (input.bidAmount < locals.auction.core.salePrice) { output.errorCode = static_cast(EAuctionError::BidTooLow); return; @@ -1885,8 +1937,8 @@ struct NOST : public ContractBase locals.participantKey = {input.auctionId, qpi.invocator()}; locals.participantExists = state.get().participants.get(locals.participantKey, locals.participantData); locals.previousEscrow = locals.participantExists ? locals.participantData.escrowedAmount : 0; - locals.mustRecomputeHighestBid = - locals.participantExists && locals.auction.highestBidder == qpi.invocator() && input.bidAmount <= locals.auction.highestBidPrice; + locals.mustRecomputeHighestBid = locals.participantExists && locals.auction.core.highestBidder == qpi.invocator() && + input.bidAmount <= locals.auction.core.highestBidPrice; locals.participantData.escrowedAmount = locals.requiredEscrow; locals.participantData.requestedQuantity = input.effectiveQuantity; @@ -1896,18 +1948,18 @@ struct NOST : public ContractBase locals.participantData.participant = qpi.invocator(); locals.participantData.isWinningBid = 0; - if (input.bidAmount > locals.auction.highestBidPrice) + if (input.bidAmount > locals.auction.core.highestBidPrice) { - locals.auction.highestBidder = qpi.invocator(); - locals.auction.highestBidPrice = input.bidAmount; - locals.auction.highestBidQuantity = input.effectiveQuantity; - locals.auction.highestBidAmount = locals.requiredEscrow; + locals.auction.core.highestBidder = qpi.invocator(); + locals.auction.core.highestBidPrice = input.bidAmount; + locals.auction.core.highestBidQuantity = input.effectiveQuantity; + locals.auction.core.highestBidAmount = locals.requiredEscrow; } - locals.auction.lastBidAt = input.currentDate; - if ((locals.auction.auctionDurationSeconds - input.elapsedSeconds) <= NOST_AUCTION_EXTENSION_SECONDS) + locals.auction.core.lastBidAt = input.currentDate; + if ((locals.auction.core.auctionDurationSeconds - input.elapsedSeconds) <= NOST_AUCTION_EXTENSION_SECONDS) { - locals.auction.auctionDurationSeconds = sadd(locals.auction.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); + locals.auction.core.auctionDurationSeconds = sadd(locals.auction.core.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); } if (state.mut().participants.set(locals.participantKey, locals.participantData) == NULL_INDEX) @@ -1951,7 +2003,8 @@ struct NOST : public ContractBase return; } - if (locals.auction.quantityForSale == 0 || locals.auction.quantityForSale < locals.auction.minimumPurchaseQuantity || input.bidAmount == 0) + if (locals.auction.core.quantityForSale == 0 || locals.auction.core.quantityForSale < locals.auction.core.minimumPurchaseQuantity || + input.bidAmount == 0) { output.errorCode = static_cast(EAuctionError::InvalidInput); return; @@ -1964,15 +2017,15 @@ struct NOST : public ContractBase return; } - if (locals.auction.highestBidPrice == 0) + if (locals.auction.core.highestBidPrice == 0) { - if (input.bidAmount < locals.auction.initialPrice) + if (input.bidAmount < locals.auction.core.initialPrice) { output.errorCode = static_cast(EAuctionError::BidTooLow); return; } } - else if (input.bidAmount < sadd(locals.auction.highestBidPrice, locals.auction.minimumBidIncrement)) + else if (input.bidAmount < sadd(locals.auction.core.highestBidPrice, locals.auction.core.minimumBidIncrement)) { output.errorCode = static_cast(EAuctionError::BidTooLow); return; @@ -1988,16 +2041,16 @@ struct NOST : public ContractBase } locals.participantData.escrowedAmount = locals.requiredEscrow; - locals.participantData.requestedQuantity = locals.auction.quantityForSale; + 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.isWinningBid = 0; - if (!isZero(locals.auction.highestBidder)) + if (!isZero(locals.auction.core.highestBidder)) { - locals.highestBidderKey = {locals.auction.auctionId, locals.auction.highestBidder}; + locals.highestBidderKey = {locals.auction.core.auctionId, locals.auction.core.highestBidder}; locals.highestBidderExists = state.get().participants.get(locals.highestBidderKey, locals.previousHighestBidderData); } if (locals.highestBidderExists && locals.previousHighestBidderData.participant != qpi.invocator()) @@ -2010,17 +2063,17 @@ struct NOST : public ContractBase } locals.participantData.isWinningBid = 1; - locals.auction.highestBidder = qpi.invocator(); - locals.auction.highestBidPrice = input.bidAmount; - locals.auction.highestBidQuantity = locals.auction.quantityForSale; - locals.auction.highestBidAmount = locals.requiredEscrow; + 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.lastBidAt = input.currentDate; - if ((locals.auction.auctionDurationSeconds - input.elapsedSeconds) <= NOST_AUCTION_EXTENSION_SECONDS) + locals.auction.core.lastBidAt = input.currentDate; + if ((locals.auction.core.auctionDurationSeconds - input.elapsedSeconds) <= NOST_AUCTION_EXTENSION_SECONDS) { - locals.auction.auctionDurationSeconds = sadd(locals.auction.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); + locals.auction.core.auctionDurationSeconds = sadd(locals.auction.core.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); } - if (locals.auction.buyNowPrice > 0 && input.bidAmount >= locals.auction.buyNowPrice) + if (locals.auction.core.buyNowPrice > 0 && input.bidAmount >= locals.auction.core.buyNowPrice) { locals.finalizeImmediately = 1; } @@ -2145,15 +2198,15 @@ struct NOST : public ContractBase return; } - if (locals.auction.type != EAuctionType::Batch || locals.auction.status != EAuctionStatus::Active) + if (locals.auction.core.type != EAuctionType::Batch || locals.auction.core.status != EAuctionStatus::Active) { return; } // Resolve the single sellable lot entry that represents the batch asset and quantity in escrow. - for (locals.lotItemIndex = 0; locals.lotItemIndex < locals.auction.auctionLotItems.capacity(); ++locals.lotItemIndex) + for (locals.lotItemIndex = 0; locals.lotItemIndex < locals.auction.core.auctionLotItems.capacity(); ++locals.lotItemIndex) { - locals.batchLotItem = locals.auction.auctionLotItems.get(locals.lotItemIndex); + locals.batchLotItem = locals.auction.core.auctionLotItems.get(locals.lotItemIndex); if (!isZeroAsset(locals.batchLotItem.asset) && locals.batchLotItem.quantity > 0) { locals.lotItemFound = 1; @@ -2166,7 +2219,7 @@ struct NOST : public ContractBase } // Repeatedly pick the best remaining bid, allocate available quantity, and collect the winning payment. - locals.remainingQuantity = locals.auction.quantityForSale; + locals.remainingQuantity = locals.auction.core.quantityForSale; while (locals.remainingQuantity > 0) { locals.bestParticipantFound = 0; @@ -2252,10 +2305,10 @@ struct NOST : public ContractBase } // Return any unsold batch quantity to the seller when demand did not consume the entire lot. - if (locals.soldQuantity < locals.auction.quantityForSale) + if (locals.soldQuantity < locals.auction.core.quantityForSale) { qpi.transferShareOwnershipAndPossession(locals.batchLotItem.asset.assetName, locals.batchLotItem.asset.issuer, SELF, SELF, - locals.auction.quantityForSale - locals.soldQuantity, locals.auction.seller); + 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. @@ -2263,15 +2316,15 @@ struct NOST : public ContractBase CALL(DistributeAuctionRevenue, locals.distributeAuctionRevenueInput, locals.distributeAuctionRevenueOutput); if (locals.distributeAuctionRevenueOutput.sellerPayout > 0) { - qpi.transfer(locals.auction.seller, locals.distributeAuctionRevenueOutput.sellerPayout); + qpi.transfer(locals.auction.core.seller, locals.distributeAuctionRevenueOutput.sellerPayout); } // Persist the final sold quantity and close the auction as settled. - locals.auction.allocatedQuantity = locals.soldQuantity; - locals.auction.status = EAuctionStatus::Finalized; - locals.auction.settledAt = input.currentDate; - state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); - addClosedAuctionToHistory(state, locals.auction.auctionId); + locals.auction.core.allocatedQuantity = locals.soldQuantity; + locals.auction.core.status = EAuctionStatus::Finalized; + locals.auction.core.settledAt = input.currentDate; + state.mut().auctionList.replace(locals.auction.core.auctionId, locals.auction); + addClosedAuctionToHistory(state, locals.auction.core.auctionId); output.success = 1; } @@ -2286,20 +2339,20 @@ struct NOST : public ContractBase return; } - if (locals.auction.type != EAuctionType::Standard) + if (locals.auction.core.type != EAuctionType::Standard) { return; } - if (!isZero(locals.auction.highestBidder)) + if (!isZero(locals.auction.core.highestBidder)) { - locals.highestBidderKey = {locals.auction.auctionId, locals.auction.highestBidder}; + locals.highestBidderKey = {locals.auction.core.auctionId, locals.auction.core.highestBidder}; locals.highestBidderExists = state.get().participants.get(locals.highestBidderKey, locals.highestBidderData); } if (locals.highestBidderExists && locals.highestBidderData.escrowedAmount > 0) { - locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; locals.rollbackAuctionLotAssetsInput.recipient = locals.highestBidderData.participant; CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); @@ -2307,35 +2360,35 @@ struct NOST : public ContractBase CALL(DistributeAuctionRevenue, locals.distributeAuctionRevenueInput, locals.distributeAuctionRevenueOutput); if (locals.distributeAuctionRevenueOutput.sellerPayout > 0) { - qpi.transfer(locals.auction.seller, locals.distributeAuctionRevenueOutput.sellerPayout); + qpi.transfer(locals.auction.core.seller, locals.distributeAuctionRevenueOutput.sellerPayout); } - locals.highestBidderData.allocatedQuantity = locals.auction.quantityForSale; + locals.highestBidderData.allocatedQuantity = locals.auction.core.quantityForSale; locals.highestBidderData.isWinningBid = 1; locals.highestBidderData.escrowedAmount = 0; state.mut().participants.replace(locals.highestBidderKey, locals.highestBidderData); - locals.auction.allocatedQuantity = locals.auction.quantityForSale; + locals.auction.core.allocatedQuantity = locals.auction.core.quantityForSale; locals.lotSold = 1; } else { - locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.auctionLotItems; - locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.seller; + locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.core.seller; CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); - locals.auction.allocatedQuantity = 0; + locals.auction.core.allocatedQuantity = 0; } - locals.auction.status = EAuctionStatus::Finalized; - locals.auction.settledAt = input.currentDate; + locals.auction.core.status = EAuctionStatus::Finalized; + locals.auction.core.settledAt = input.currentDate; if (!locals.lotSold) { - locals.auction.highestBidAmount = 0; - locals.auction.highestBidPrice = 0; - locals.auction.highestBidQuantity = 0; - locals.auction.highestBidder = NULL_ID; + locals.auction.core.highestBidAmount = 0; + locals.auction.core.highestBidPrice = 0; + locals.auction.core.highestBidQuantity = 0; + locals.auction.core.highestBidder = NULL_ID; } - state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); - addClosedAuctionToHistory(state, locals.auction.auctionId); + state.mut().auctionList.replace(locals.auction.core.auctionId, locals.auction); + addClosedAuctionToHistory(state, locals.auction.core.auctionId); output.success = 1; } @@ -2350,14 +2403,14 @@ struct NOST : public ContractBase return; } - if (locals.auction.type != EAuctionType::Standard || locals.auction.status != EAuctionStatus::PendingSellerDecision) + if (locals.auction.core.type != EAuctionType::Standard || locals.auction.core.status != EAuctionStatus::PendingSellerDecision) { return; } - if (!isZero(locals.auction.highestBidder)) + if (!isZero(locals.auction.core.highestBidder)) { - locals.highestBidderKey = {locals.auction.auctionId, locals.auction.highestBidder}; + locals.highestBidderKey = {locals.auction.core.auctionId, locals.auction.core.highestBidder}; locals.highestBidderExists = state.get().participants.get(locals.highestBidderKey, locals.highestBidderData); } @@ -2371,19 +2424,19 @@ struct NOST : public ContractBase state.mut().participants.replace(locals.highestBidderKey, locals.highestBidderData); } - locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.auctionLotItems; - locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.seller; + locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.core.seller; CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); - locals.auction.allocatedQuantity = 0; - locals.auction.highestBidAmount = 0; - locals.auction.highestBidPrice = 0; - locals.auction.highestBidQuantity = 0; - locals.auction.highestBidder = NULL_ID; - locals.auction.status = EAuctionStatus::Finalized; - locals.auction.settledAt = input.currentDate; - state.mut().auctionList.replace(locals.auction.auctionId, locals.auction); - addClosedAuctionToHistory(state, locals.auction.auctionId); + 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.status = EAuctionStatus::Finalized; + locals.auction.core.settledAt = input.currentDate; + state.mut().auctionList.replace(locals.auction.core.auctionId, locals.auction); + addClosedAuctionToHistory(state, locals.auction.core.auctionId); output.success = 1; } @@ -2581,17 +2634,17 @@ struct NOST : public ContractBase return; } - locals.auction.auctionId = id::randomValue(); - locals.auction.quantityForSale = locals.resolvedQuantityForSale; - locals.auction.minimumPurchaseQuantity = locals.resolvedMinimumPurchaseQuantity; - locals.auction.initialPrice = input.initialPrice; - locals.auction.salePrice = input.salePrice; - locals.auction.minimumBidIncrement = input.minimumBidIncrement; - locals.auction.buyNowPrice = input.buyNowPrice; - locals.auction.auctionDurationSeconds = smul(static_cast(input.durationDays), NOST_SECONDS_PER_DAY); - locals.auction.createdAt = qpi.now(); - locals.auction.lastBidAt = locals.auction.createdAt; - locals.auction.seller = qpi.invocator(); + locals.auction.core.auctionId = id::randomValue(); + 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(); for (locals.requiredAccessAssetIndex = 0; locals.requiredAccessAssetIndex < input.requiredAccessAssets.capacity(); ++locals.requiredAccessAssetIndex) { @@ -2600,7 +2653,7 @@ struct NOST : public ContractBase locals.auction.requiredAccessAssets.add(input.requiredAccessAssets.get(locals.requiredAccessAssetIndex)); } } - locals.auction.auctionLotItems = input.auctionLotItems; + locals.auction.core.auctionLotItems = input.auctionLotItems; for (locals.allowedWalletIndex = 0; locals.allowedWalletIndex < input.allowedBidderWallets.capacity(); ++locals.allowedWalletIndex) { if (!isZero(input.allowedBidderWallets.get(locals.allowedWalletIndex))) @@ -2608,12 +2661,12 @@ struct NOST : public ContractBase locals.auction.allowedBidderWallets.add(input.allowedBidderWallets.get(locals.allowedWalletIndex)); } } - locals.auction.metadataIpfsCid = input.metadataIpfsCid; - locals.auction.type = static_cast(input.auctionType); - locals.auction.visibility = static_cast(input.auctionVisibility); - locals.auction.status = EAuctionStatus::Active; + 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 (state.mut().auctionList.set(locals.auction.auctionId, locals.auction) == NULL_INDEX) + if (state.mut().auctionList.set(locals.auction.core.auctionId, locals.auction) == NULL_INDEX) { locals.rollbackAuctionLotAssetsInput.auctionLotItems = input.auctionLotItems; locals.rollbackAuctionLotAssetsInput.recipient = qpi.invocator(); @@ -2634,7 +2687,7 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.requiredFee); } - output.auctionId = locals.auction.auctionId; + output.auctionId = locals.auction.core.auctionId; output.errorCode = static_cast(EAuctionError::Success); } @@ -2668,7 +2721,7 @@ struct NOST : public ContractBase return; } - if (locals.auction.status != EAuctionStatus::Active) + if (locals.auction.core.status != EAuctionStatus::Active) { if (qpi.invocationReward() > 0) { @@ -2678,7 +2731,7 @@ struct NOST : public ContractBase return; } - if (locals.auction.seller == qpi.invocator()) + if (locals.auction.core.seller == qpi.invocator()) { if (qpi.invocationReward() > 0) { @@ -2689,8 +2742,8 @@ struct NOST : public ContractBase } locals.currentDate = qpi.now(); - diffDateInSecond(locals.auction.createdAt, locals.currentDate, locals.elapsedSeconds); - if (locals.elapsedSeconds >= locals.auction.auctionDurationSeconds) + diffDateInSecond(locals.auction.core.createdAt, locals.currentDate, locals.elapsedSeconds); + if (locals.elapsedSeconds >= locals.auction.core.auctionDurationSeconds) { if (qpi.invocationReward() > 0) { @@ -2700,11 +2753,11 @@ struct NOST : public ContractBase return; } - if (locals.auction.visibility == EAuctionVisibility::Private) + if (locals.auction.core.visibility == EAuctionVisibility::Private) { if (locals.auction.requiredAccessAssets.population() > 0) { - locals.hasRequiredAccessAssetInput.auction = locals.auction; + locals.hasRequiredAccessAssetInput.auctionId = input.auctionId; CALL(HasRequiredAccessAsset, locals.hasRequiredAccessAssetInput, locals.hasRequiredAccessAssetOutput); locals.hasAccess = locals.hasRequiredAccessAssetOutput.hasRequiredAccessAsset; } @@ -2724,7 +2777,7 @@ struct NOST : public ContractBase } } - switch (locals.auction.type) + switch (locals.auction.core.type) { case EAuctionType::Batch: locals.processBatchBidInput.auctionId = input.auctionId; @@ -2796,7 +2849,7 @@ struct NOST : public ContractBase return; } - if (locals.auction.status != EAuctionStatus::Active) + if (locals.auction.core.status != EAuctionStatus::Active) { if (qpi.invocationReward() > 0) { @@ -2806,7 +2859,7 @@ struct NOST : public ContractBase return; } - if (locals.auction.seller != qpi.invocator()) + if (locals.auction.core.seller != qpi.invocator()) { if (qpi.invocationReward() > 0) { @@ -2816,7 +2869,7 @@ struct NOST : public ContractBase return; } - if (locals.auction.highestBidAmount > 0) + if (locals.auction.core.highestBidAmount > 0) { if (qpi.invocationReward() > 0) { @@ -2826,10 +2879,10 @@ struct NOST : public ContractBase return; } - locals.cancellationBaseAmount = locals.auction.salePrice; - if (locals.auction.type == EAuctionType::Batch) + locals.cancellationBaseAmount = locals.auction.core.salePrice; + if (locals.auction.core.type == EAuctionType::Batch) { - locals.cancellationBaseAmount = smul(locals.auction.salePrice, locals.auction.quantityForSale); + locals.cancellationBaseAmount = smul(locals.auction.core.salePrice, locals.auction.core.quantityForSale); } output.cancellationFee = div(smul(locals.cancellationBaseAmount, state.get().auctionCancellationFeeBasisPoints), 10000ULL); @@ -2862,15 +2915,15 @@ struct NOST : public ContractBase } state.mut().participants.cleanupIfNeeded(); - locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.auctionLotItems; - locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.seller; + locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; + locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.core.seller; CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); locals.currentDate = qpi.now(); - locals.auction.status = EAuctionStatus::Cancelled; - locals.auction.settledAt = locals.currentDate; + locals.auction.core.status = EAuctionStatus::Cancelled; + locals.auction.core.settledAt = locals.currentDate; state.mut().auctionList.replace(input.auctionId, locals.auction); - addClosedAuctionToHistory(state, locals.auction.auctionId); + addClosedAuctionToHistory(state, locals.auction.core.auctionId); locals.distributeAuctionServiceFeeInput.feeAmount = output.cancellationFee; CALL(DistributeAuctionServiceFee, locals.distributeAuctionServiceFeeInput, locals.distributeAuctionServiceFeeOutput); @@ -2915,20 +2968,20 @@ struct NOST : public ContractBase return; } - if (locals.auction.seller != qpi.invocator()) + if (locals.auction.core.seller != qpi.invocator()) { output.errorCode = static_cast(EAuctionError::Forbidden); return; } - if (locals.auction.type != EAuctionType::Standard || locals.auction.status != EAuctionStatus::PendingSellerDecision) + if (locals.auction.core.type != EAuctionType::Standard || locals.auction.core.status != EAuctionStatus::PendingSellerDecision) { output.errorCode = static_cast(EAuctionError::AuctionClosed); return; } locals.currentDate = qpi.now(); - if (!state.get().isAuctionTimerPaused && locals.auction.sellerDecisionDeadline <= locals.currentDate) + if (!state.get().isAuctionTimerPaused && locals.auction.core.sellerDecisionDeadline <= locals.currentDate) { locals.finalizeStandardAuctionInput.auctionId = input.auctionId; locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; @@ -3064,9 +3117,39 @@ struct NOST : public ContractBase /** * @brief Returns the stored state of one auction. - * @note The response contains the full persistent `AuctionData` record for the requested auction identifier. + * @note The response contains a serializable auction view; access-control sets are returned as fixed arrays with counts. */ - PUBLIC_FUNCTION(GetAuction) { state.get().auctionList.get(input.auctionId, output.auction); } + PUBLIC_FUNCTION_WITH_LOCALS(GetAuction) + { + output.found = 0; + if (!state.get().auctionList.get(input.auctionId, locals.auction)) + { + return; + } + + output.found = 1; + output.auction.core = locals.auction.core; + + 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)) + { + locals.requiredAccessAsset = locals.auction.requiredAccessAssets.key(locals.requiredAccessAssetSetIndex); + output.auction.requiredAccessAssets.set(output.auction.requiredAccessAssetCount, locals.requiredAccessAsset); + output.auction.requiredAccessAssetCount = sadd(output.auction.requiredAccessAssetCount, 1ULL); + } + + 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)) + { + 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); + } + } /** * @brief Returns the stored bid state of one wallet in one auction. From 02ded974e4164d115f598155a9c8e2dcda79624b Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 29 Apr 2026 21:04:07 +0300 Subject: [PATCH 34/59] Refactor `ContractNostromoAuction` tests: replace nested auction fields with `AuctionCore` references, update related assertions, and introduce `seedUser` to streamline user initialization logic. --- src/contracts/Nostromo.h | 2 + test/contract_nostromo.cpp | 159 +++++++++++++++++++------------------ 2 files changed, 82 insertions(+), 79 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index de726659e..b536fd9b4 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -3226,6 +3226,8 @@ struct NOST : public ContractBase /** * @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) { diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index 607d30991..cf5b8af47 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -178,9 +178,8 @@ class ContractTestingNOST : protected ContractTesting input.newManagingContractIndex = contractIndex; syncCachedQxTransferFee(); - increaseEnergy(NOST_CONTRACT_ID, getCachedQxTransferFee()); - ensureUser(owner); - invokeUserProcedure(NOST_CONTRACT_INDEX, 4, input, output, owner, 0); + seedUser(owner, getCachedQxTransferFee()); + invokeUserProcedure(NOST_CONTRACT_INDEX, 4, input, output, owner, getCachedQxTransferFee()); return output; } @@ -465,7 +464,7 @@ TEST(ContractNostromoAuction, InitialStateAndGettersAuction) const auto participantOutput = nostromo.getParticipant(missingAuction, missingParticipant); const auto launchPause = nostromo.getTicksBeforeAuctionLaunch(); - EXPECT_TRUE(isZero(auctionOutput.auction.auctionId)); + EXPECT_TRUE(isZero(auctionOutput.auction.core.auctionId)); EXPECT_EQ(participantOutput.found, 0); EXPECT_EQ(launchPause.ticks, 0U); @@ -523,18 +522,18 @@ TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) EXPECT_FALSE(isZero(output.auctionId)); const auto auction = nostromo.getAuction(output.auctionId).auction; - EXPECT_EQ(auction.auctionId, output.auctionId); - EXPECT_EQ(auction.quantityForSale, 9ULL); - EXPECT_EQ(auction.minimumPurchaseQuantity, 0ULL); - EXPECT_EQ(auction.salePrice, 25ULL); - EXPECT_EQ(auction.auctionDurationSeconds, NOST_SECONDS_PER_DAY); - EXPECT_EQ(auction.seller, seller); - EXPECT_EQ(auction.type, NOST::EAuctionType::Batch); - EXPECT_EQ(auction.visibility, NOST::EAuctionVisibility::Public); - EXPECT_EQ(auction.status, NOST::EAuctionStatus::Active); - EXPECT_EQ(auction.auctionLotItems.get(0).asset, asset); - EXPECT_EQ(auction.auctionLotItems.get(0).quantity, 9); - EXPECT_EQ(auction.metadataIpfsCid.get(0), 'b'); + EXPECT_EQ(auction.core.auctionId, output.auctionId); + EXPECT_EQ(auction.core.quantityForSale, 9ULL); + EXPECT_EQ(auction.core.minimumPurchaseQuantity, 0ULL); + 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); } @@ -559,16 +558,16 @@ TEST(ContractNostromoAuction, CreateStandardBundleAuctionEscrowsMixedLotAuction) ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); const auto auction = nostromo.getAuction(output.auctionId).auction; - EXPECT_EQ(auction.quantityForSale, 1ULL); - EXPECT_EQ(auction.minimumPurchaseQuantity, 1ULL); - EXPECT_EQ(auction.initialPrice, 100ULL); - EXPECT_EQ(auction.salePrice, 150ULL); - EXPECT_EQ(auction.minimumBidIncrement, 5ULL); - EXPECT_EQ(auction.type, NOST::EAuctionType::Standard); - EXPECT_EQ(auction.auctionLotItems.get(0).asset, assetA); - EXPECT_EQ(auction.auctionLotItems.get(0).quantity, 2); - EXPECT_EQ(auction.auctionLotItems.get(1).asset, assetB); - EXPECT_EQ(auction.auctionLotItems.get(1).quantity, 3); + EXPECT_EQ(auction.core.quantityForSale, 1ULL); + EXPECT_EQ(auction.core.minimumPurchaseQuantity, 1ULL); + EXPECT_EQ(auction.core.initialPrice, 100ULL); + EXPECT_EQ(auction.core.salePrice, 150ULL); + EXPECT_EQ(auction.core.minimumBidIncrement, 5ULL); + EXPECT_EQ(auction.core.type, NOST::EAuctionType::Standard); + EXPECT_EQ(auction.core.auctionLotItems.get(0).asset, assetA); + EXPECT_EQ(auction.core.auctionLotItems.get(0).quantity, 2); + EXPECT_EQ(auction.core.auctionLotItems.get(1).asset, assetB); + EXPECT_EQ(auction.core.auctionLotItems.get(1).quantity, 3); EXPECT_EQ(nostromo.sharesManagedBy(assetA, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 2); EXPECT_EQ(nostromo.sharesManagedBy(assetB, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 3); } @@ -593,9 +592,10 @@ TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); const auto auction = nostromo.getAuction(output.auctionId).auction; - EXPECT_EQ(auction.visibility, NOST::EAuctionVisibility::Private); - EXPECT_TRUE(auction.allowedBidderWallets.contains(allowedBidder)); - EXPECT_EQ(auction.requiredAccessAssets.population(), 0U); + 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); } { @@ -619,9 +619,10 @@ TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); const auto auction = nostromo.getAuction(output.auctionId).auction; - EXPECT_EQ(auction.visibility, NOST::EAuctionVisibility::Private); - EXPECT_EQ(auction.allowedBidderWallets.population(), 0U); - EXPECT_TRUE(auction.requiredAccessAssets.contains(gateAsset)); + EXPECT_EQ(auction.core.visibility, NOST::EAuctionVisibility::Private); + EXPECT_EQ(auction.allowedBidderWalletCount, 0U); + EXPECT_EQ(auction.requiredAccessAssetCount, 1U); + EXPECT_EQ(auction.requiredAccessAssets.get(0), gateAsset); EXPECT_GT(nostromo.plainShares(gateAsset, gatedBidder), 0); } } @@ -880,9 +881,9 @@ TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAucti ASSERT_EQ(bidB.errorCode, static_cast(NOST::EAuctionError::Success)); auto auction = nostromo.getAuction(createOutput.auctionId).auction; - EXPECT_EQ(auction.highestBidder, bidderA); - EXPECT_EQ(auction.highestBidPrice, 20ULL); - EXPECT_EQ(auction.highestBidAmount, 40ULL); + 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.auctionId, 2, 14, 28); EXPECT_EQ(bidA2.errorCode, static_cast(NOST::EAuctionError::Success)); @@ -890,9 +891,9 @@ TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAucti EXPECT_EQ(bidA2.refundedAmount, 40ULL); auction = nostromo.getAuction(createOutput.auctionId).auction; - EXPECT_EQ(auction.highestBidder, bidderB); - EXPECT_EQ(auction.highestBidPrice, 15ULL); - EXPECT_EQ(auction.highestBidAmount, 45ULL); + EXPECT_EQ(auction.core.highestBidder, bidderB); + EXPECT_EQ(auction.core.highestBidPrice, 15ULL); + EXPECT_EQ(auction.core.highestBidAmount, 45ULL); const auto participantA = nostromo.getParticipant(createOutput.auctionId, bidderA); ASSERT_EQ(participantA.found, 1); @@ -923,7 +924,7 @@ TEST(ContractNostromoAuction, PlaceBatchBidExtendsAuctionNearEndAuction) ASSERT_EQ(bidOutput.errorCode, static_cast(NOST::EAuctionError::Success)); const auto auction = nostromo.getAuction(createOutput.auctionId).auction; - EXPECT_EQ(auction.auctionDurationSeconds, NOST_SECONDS_PER_DAY + NOST_AUCTION_EXTENSION_SECONDS); + EXPECT_EQ(auction.core.auctionDurationSeconds, NOST_SECONDS_PER_DAY + NOST_AUCTION_EXTENSION_SECONDS); } TEST(ContractNostromoAuction, PlaceStandardBidValidatesRefundsAndPauseAuction) @@ -1068,8 +1069,8 @@ TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) const auto auction = nostromo.getAuction(createOutput.auctionId).auction; const auto participant = nostromo.getParticipant(createOutput.auctionId, bidder); - EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(auction.allocatedQuantity, 1ULL); + 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); @@ -1109,8 +1110,8 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF const auto participantB = nostromo.getParticipant(createOutput.auctionId, bidderB); const auto participantC = nostromo.getParticipant(createOutput.auctionId, bidderC); - EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(auction.allocatedQuantity, 4ULL); + 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); @@ -1146,8 +1147,8 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); const auto auction = nostromo.getAuction(createOutput.auctionId).auction; - EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(auction.allocatedQuantity, 2ULL); + 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); } @@ -1170,9 +1171,9 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); const auto auction = nostromo.getAuction(createOutput.auctionId).auction; - EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(auction.allocatedQuantity, 0ULL); - EXPECT_TRUE(isZero(auction.highestBidder)); + 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); } @@ -1193,15 +1194,15 @@ TEST(ContractNostromoAuction, WeeklyPauseShiftsActiveAuctionDeadlineAuction) nostromo.setNow(2026, 1, 7, 11, 40, 0); nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Active); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.status, NOST::EAuctionStatus::Active); nostromo.setNow(2026, 1, 7, 12, 0, 0); nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Active); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.status, NOST::EAuctionStatus::Active); nostromo.setNow(2026, 1, 7, 12, 10, 1); nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(nostromo.managedShares(asset, seller), 1); } @@ -1224,8 +1225,8 @@ TEST(ContractNostromoAuction, EndTickSkipsAuctionProcessingAtBootstrapTimeAuctio nostromo.advanceAndEndTick(0); const auto auction = nostromo.getAuction(createOutput.auctionId).auction; - EXPECT_EQ(auction.status, NOST::EAuctionStatus::Active); - EXPECT_EQ(auction.allocatedQuantity, 0ULL); + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Active); + EXPECT_EQ(auction.core.allocatedQuantity, 0ULL); EXPECT_EQ(nostromo.managedShares(asset, seller), 0); } @@ -1247,11 +1248,11 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY) * 1000ULL); auto auction = nostromo.getAuction(createOutput.auctionId).auction; - const auto originalSellerDecisionDeadline = auction.sellerDecisionDeadline; - ASSERT_EQ(auction.status, NOST::EAuctionStatus::PendingSellerDecision); - EXPECT_EQ(auction.sellerDecisionDeadline.getHour(), 9); - EXPECT_EQ(auction.sellerDecisionDeadline.getMinute(), 0); - EXPECT_EQ(auction.sellerDecisionDeadline.getSecond(), 0); + 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(); @@ -1264,29 +1265,29 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) nostromo.setNow(2026, 1, 9, 9, 8, 10); nostromo.advanceAndEndTick(0); auction = nostromo.getAuction(createOutput.auctionId).auction; - ASSERT_EQ(auction.status, NOST::EAuctionStatus::PendingSellerDecision); - EXPECT_EQ(auction.sellerDecisionDeadline, originalSellerDecisionDeadline); + ASSERT_EQ(auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(auction.core.sellerDecisionDeadline, originalSellerDecisionDeadline); nostromo.advanceTicks(launchPauseTicksAfterBeginEpoch - 2); auction = nostromo.getAuction(createOutput.auctionId).auction; - ASSERT_EQ(auction.status, NOST::EAuctionStatus::PendingSellerDecision); + ASSERT_EQ(auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); EXPECT_EQ(nostromo.getTicksBeforeAuctionLaunch().ticks, 0U); - EXPECT_GT(auction.sellerDecisionDeadline, originalSellerDecisionDeadline); + EXPECT_GT(auction.core.sellerDecisionDeadline, originalSellerDecisionDeadline); - auto shiftedDeadline = auction.sellerDecisionDeadline; + 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.auctionId).auction.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); - shiftedDeadline = auction.sellerDecisionDeadline; + 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.auctionId).auction; - EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); } @@ -1323,8 +1324,8 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); const auto auction = nostromo.getAuction(createOutput.auctionId).auction; - EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(auction.allocatedQuantity, 1ULL); + 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, expectedRevenue.sellerPayout); @@ -1363,14 +1364,14 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); const auto forbidden = nostromo.resolvePendingStandardAuction(id(999, 999, 999, 999), createOutput.auctionId, true); EXPECT_EQ(forbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); const auto acceptOutput = nostromo.resolvePendingStandardAuction(seller, createOutput.auctionId, true); EXPECT_EQ(acceptOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); } @@ -1396,9 +1397,9 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction const auto auction = nostromo.getAuction(createOutput.auctionId).auction; const auto participant = nostromo.getParticipant(createOutput.auctionId, bidder); - EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(auction.allocatedQuantity, 0ULL); - EXPECT_TRUE(isZero(auction.highestBidder)); + 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); @@ -1421,13 +1422,13 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); nostromo.advanceAndEndTick((NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS + 1ULL) * 1000ULL); const auto auction = nostromo.getAuction(createOutput.auctionId).auction; const auto participant = nostromo.getParticipant(createOutput.auctionId, bidder); - EXPECT_EQ(auction.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(auction.allocatedQuantity, 1ULL); + 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); @@ -1465,7 +1466,7 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) EXPECT_EQ(cancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); EXPECT_EQ(cancelOutput.refundedAmount, 0ULL); EXPECT_EQ(cancelOutput.cancellationFee, 1000ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Cancelled); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.status, NOST::EAuctionStatus::Cancelled); EXPECT_EQ(nostromo.managedShares(asset, seller), 10); EXPECT_EQ(getBalance(seller) - sellerBefore, 0); if (routeMode != 0) @@ -1512,7 +1513,7 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) EXPECT_EQ(cancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); EXPECT_EQ(cancelOutput.refundedAmount, 0ULL); EXPECT_EQ(cancelOutput.cancellationFee, 1000ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Cancelled); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.status, NOST::EAuctionStatus::Cancelled); EXPECT_EQ(nostromo.managedShares(asset, seller), 1); EXPECT_EQ(getBalance(seller) - sellerBefore, 0); if (routeMode != 0) @@ -1563,7 +1564,7 @@ TEST(ContractNostromoAuction, CancelAuctionRejectsAfterBidAuction) const auto closed = nostromo.cancelAuction(seller, createOutput.auctionId, 2); EXPECT_EQ(closed.errorCode, static_cast(NOST::EAuctionError::Forbidden)); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.status, NOST::EAuctionStatus::Active); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.status, NOST::EAuctionStatus::Active); } TEST(ContractNostromoAuction, CancelAuctionRejectsInvalidCasesWithoutBidsAuction) From 8eddac80751b81948cd30cb855ab0ca38c51b8d4 Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 29 Apr 2026 21:30:54 +0300 Subject: [PATCH 35/59] Introduce extended auction handling methods and storage validation: add `placeBidWithFundedReward`, extend `transferManagedShares` with reward options, integrate private auction access lists, and ensure proper handling for scenarios like storage full conditions. Update tests accordingly. --- test/contract_nostromo.cpp | 666 ++++++++++++++++++++++++++++++++++++- 1 file changed, 661 insertions(+), 5 deletions(-) diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index cf5b8af47..e936c9bf1 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -150,6 +150,19 @@ class ContractTestingNOST : protected ContractTesting return output; } + NOST::PlaceBid_output placeBidWithFundedReward(const id& bidder, const id& auctionId, uint64 quantity, uint64 bidAmount, sint64 reward) + { + NOST::PlaceBid_input input{}; + NOST::PlaceBid_output output{}; + + input.auctionId = auctionId; + input.quantity = quantity; + input.bidAmount = bidAmount; + + invokeUserProcedure(NOST_CONTRACT_INDEX, 2, input, output, bidder, reward); + return output; + } + NOST::CancelAuction_output cancelAuction(const id& seller, const id& auctionId, sint64 reward) { NOST::CancelAuction_input input{}; @@ -168,7 +181,22 @@ class ContractTestingNOST : protected ContractTesting return output; } - NOST::TransferShareManagementRights_output transferManagedShares(const id& owner, const Asset& asset, sint64 numberOfShares, uint32 contractIndex) + 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{}; @@ -177,12 +205,16 @@ class ContractTestingNOST : protected ContractTesting input.numberOfShares = numberOfShares; input.newManagingContractIndex = contractIndex; - syncCachedQxTransferFee(); - seedUser(owner, getCachedQxTransferFee()); - invokeUserProcedure(NOST_CONTRACT_INDEX, 4, input, output, owner, getCachedQxTransferFee()); + 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, const id& auctionId, bool acceptSale) { NOST::ResolvePendingStandardAuction_input input{}; @@ -271,6 +303,24 @@ class ContractTestingNOST : protected ContractTesting 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::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]); } @@ -369,6 +419,19 @@ class ContractTestingNOST : protected ContractTesting return required; } + static Array makeFullLot(const Asset& asset, sint64 quantity) + { + Array lot{}; + for (uint64 index = 0; index < lot.capacity(); ++index) + { + NOST::AuctionLotEntry entry{}; + entry.asset = asset; + entry.quantity = quantity; + lot.set(index, entry); + } + return lot; + } + static NOST::CreateAuction_input makeBatchAuctionInput(const Asset& asset, sint64 quantity, uint64 salePrice = 10) { NOST::CreateAuction_input input{}; @@ -437,6 +500,43 @@ class ContractTestingNOST : protected ContractTesting } }; +static bool containsWallet(const Array& wallets, uint64 count, const id& wallet) +{ + for (uint64 index = 0; index < count; ++index) + { + if (wallets.get(index) == wallet) + { + return true; + } + } + return false; +} + +static bool containsAccessAsset(const Array& assets, uint64 count, const Asset& asset) +{ + for (uint64 index = 0; index < count; ++index) + { + if (assets.get(index) == asset) + { + return true; + } + } + return false; +} + +static bool containsAuctionId(const Array& auctionIds, uint64 count, const id& auctionId) +{ + const uint64 boundedCount = count < auctionIds.capacity() ? count : auctionIds.capacity(); + for (uint64 index = 0; index < boundedCount; ++index) + { + if (auctionIds.get(index) == auctionId) + { + return true; + } + } + return false; +} + TEST(ContractNostromoAuction, InitialStateAndGettersAuction) { ContractTestingNOST nostromo; @@ -467,6 +567,13 @@ TEST(ContractNostromoAuction, InitialStateAndGettersAuction) EXPECT_TRUE(isZero(auctionOutput.auction.core.auctionId)); 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); @@ -506,6 +613,81 @@ TEST(ContractNostromoAuction, TransferShareManagementRightsAuction) 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(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(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(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(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(nostromo.managedShares(asset, owner), 4); + } +} + TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) { ContractTestingNOST nostromo; @@ -572,6 +754,33 @@ TEST(ContractNostromoAuction, CreateStandardBundleAuctionEscrowsMixedLotAuction) EXPECT_EQ(nostromo.sharesManagedBy(assetB, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 3); } +TEST(ContractNostromoAuction, CreateStandardAuctionAcceptsMaximumLotEntriesAuction) +{ + ContractTestingNOST nostromo; + const id seller(25, 26, 27, 28); + const id bidder(29, 30, 31, 32); + const uint64 assetName = assetNameFromString("MAXLOT"); + const Asset asset{seller, assetName}; + + EXPECT_EQ(nostromo.issueAsset(seller, assetName, NOST_AUCTION_LOT_ITEM_NUM), static_cast(NOST_AUCTION_LOT_ITEM_NUM)); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, NOST_AUCTION_LOT_ITEM_NUM), + static_cast(NOST_AUCTION_LOT_ITEM_NUM)); + + const auto createOutput = nostromo.createAuction( + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeFullLot(asset, 1), 100, 100, 1)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); + EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), static_cast(NOST_AUCTION_LOT_ITEM_NUM)); + + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 100, 100).errorCode, static_cast(NOST::EAuctionError::Success)); + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.managedShares(asset, bidder), static_cast(NOST_AUCTION_LOT_ITEM_NUM)); + EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 0); +} + TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction) { { @@ -627,6 +836,186 @@ TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction } } +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, static_cast(NOST::EAuctionError::Success)); + + const auto auctionOutput = nostromo.getAuction(createOutput.auctionId); + EXPECT_EQ(auctionOutput.found, 1); + EXPECT_EQ(auctionOutput.auction.core.auctionId, createOutput.auctionId); + 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({accessAssetA, accessAssetB}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + + const auto auctionOutput = nostromo.getAuction(createOutput.auctionId); + EXPECT_EQ(auctionOutput.found, 1); + EXPECT_EQ(auctionOutput.auction.requiredAccessAssetCount, 2U); + EXPECT_TRUE(containsAccessAsset(auctionOutput.auction.requiredAccessAssets, auctionOutput.auction.requiredAccessAssetCount, accessAssetA)); + EXPECT_TRUE(containsAccessAsset(auctionOutput.auction.requiredAccessAssets, auctionOutput.auction.requiredAccessAssetCount, accessAssetB)); + EXPECT_EQ(auctionOutput.auction.allowedBidderWalletCount, 0U); + } + + { + ContractTestingNOST nostromo; + const id missingAuction(999, 998, 997, 996); + const auto auctionOutput = nostromo.getAuction(missingAuction); + EXPECT_EQ(auctionOutput.found, 0); + EXPECT_TRUE(isZero(auctionOutput.auction.core.auctionId)); + } +} + +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, static_cast(NOST::EAuctionError::Success)); + + const auto auction = nostromo.getAuction(createOutput.auctionId).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({accessAsset, accessAsset}); + + const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + + const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + EXPECT_EQ(auction.requiredAccessAssetCount, 1U); + EXPECT_TRUE(containsAccessAsset(auction.requiredAccessAssets, auction.requiredAccessAssetCount, accessAsset)); + } +} + +TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacityAuction) +{ + { + ContractTestingNOST nostromo; + const id seller(47, 48, 49, 50); + const id allowedBidder(30127, 31127, 32127, 33127); + const uint64 assetName = assetNameFromString("MAXWAL"); + const Asset asset{seller, assetName}; + Array allowedWallets{}; + + 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, static_cast(NOST::EAuctionError::Success)); + + const auto auctionOutput = nostromo.getAuction(createOutput.auctionId); + EXPECT_EQ(auctionOutput.auction.allowedBidderWalletCount, NOST_AUCTION_ALLOWED_WALLET_NUM); + EXPECT_TRUE(containsWallet(auctionOutput.auction.allowedBidderWallets, auctionOutput.auction.allowedBidderWalletCount, allowedBidder)); + EXPECT_EQ(nostromo.placeBid(allowedBidder, createOutput.auctionId, 1, 10, 10).errorCode, + static_cast(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, Asset{gateIssuer, 34000 + index}); + } + requiredAssets.set(NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM - 1, Asset{accessBidder, bidderAccessAssetName}); + + 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, static_cast(NOST::EAuctionError::Success)); + + const auto auctionOutput = nostromo.getAuction(createOutput.auctionId); + EXPECT_EQ(auctionOutput.auction.requiredAccessAssetCount, NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM); + EXPECT_TRUE(containsAccessAsset(auctionOutput.auction.requiredAccessAssets, auctionOutput.auction.requiredAccessAssetCount, + Asset{accessBidder, bidderAccessAssetName})); + EXPECT_EQ(nostromo.placeBid(accessBidder, createOutput.auctionId, 1, 10, 10).errorCode, + static_cast(NOST::EAuctionError::Success)); + } +} + TEST(ContractNostromoAuction, PrivateAuctionFeeIsDistributedAcrossRecipientsAuction) { const uint8 routeModes[] = {0, 1}; @@ -841,6 +1230,63 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientA } } +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.auctionId = id(10000 + index, 11000 + index, 12000 + index, 13000 + index); + auction.core.seller = seller; + auction.core.status = NOST::EAuctionStatus::Active; + ASSERT_NE(nostromo.stateData().auctionList.set(auction.core.auctionId, 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, static_cast(NOST::EAuctionError::StorageFull)); + EXPECT_TRUE(isZero(output.auctionId)); + 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, static_cast(NOST::EAuctionError::Success)); + + for (uint64 index = 0; index < NOST_AUCTION_PARTICIPANT_NUM; ++index) + { + NOST::AuctionParticipantData participant{}; + participant.participant = id(14000 + index, 15000 + index, 16000 + index, 17000 + index); + participant.bidAmount = 1; + participant.requestedQuantity = 1; + NOST::AuctionParticipantKey key{id(18000 + index, 19000 + index, 20000 + index, 21000 + index), participant.participant}; + ASSERT_NE(nostromo.stateData().participants.set(key, participant), NULL_INDEX); + } + ASSERT_EQ(nostromo.stateData().participants.population(), NOST_AUCTION_PARTICIPANT_NUM); + + const auto output = nostromo.placeBid(bidder, createOutput.auctionId, 1, 10, 10); + EXPECT_EQ(output.errorCode, static_cast(NOST::EAuctionError::StorageFull)); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.highestBidAmount, 0ULL); + EXPECT_EQ(nostromo.getParticipant(createOutput.auctionId, bidder).found, 0); +} + TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAuction) { ContractTestingNOST nostromo; @@ -1154,6 +1600,60 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF } } +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 lowerBidder(217, 218, 219, 220); + const uint64 assetName = assetNameFromString("BATTIE"); + 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, 5)); + ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + + nostromo.seedUser(earlierBidder, 100); + nostromo.seedUser(laterBidder, 100); + nostromo.seedUser(lowerBidder, 100); + const sint64 earlierBefore = getBalance(earlierBidder); + const sint64 laterBefore = getBalance(laterBidder); + const sint64 lowerBefore = getBalance(lowerBidder); + + ASSERT_EQ(nostromo.placeBidWithFundedReward(earlierBidder, createOutput.auctionId, 1, 10, 10).errorCode, + static_cast(NOST::EAuctionError::Success)); + nostromo.setNow(2026, 1, 1, 9, 0, 1); + ASSERT_EQ(nostromo.placeBidWithFundedReward(laterBidder, createOutput.auctionId, 1, 10, 10).errorCode, + static_cast(NOST::EAuctionError::Success)); + nostromo.setNow(2026, 1, 1, 9, 0, 2); + ASSERT_EQ(nostromo.placeBidWithFundedReward(lowerBidder, createOutput.auctionId, 1, 9, 9).errorCode, + static_cast(NOST::EAuctionError::Success)); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto earlier = nostromo.getParticipant(createOutput.auctionId, earlierBidder); + const auto later = nostromo.getParticipant(createOutput.auctionId, laterBidder); + const auto lower = nostromo.getParticipant(createOutput.auctionId, lowerBidder); + ASSERT_EQ(earlier.found, 1); + ASSERT_EQ(later.found, 1); + ASSERT_EQ(lower.found, 1); + EXPECT_EQ(earlier.participantData.allocatedQuantity, 1ULL); + EXPECT_EQ(later.participantData.allocatedQuantity, 0ULL); + EXPECT_EQ(lower.participantData.allocatedQuantity, 0ULL); + EXPECT_EQ(earlier.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(later.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(lower.participantData.escrowedAmount, 0ULL); + EXPECT_EQ(nostromo.managedShares(asset, earlierBidder), 1); + EXPECT_EQ(nostromo.managedShares(asset, laterBidder), 0); + EXPECT_EQ(nostromo.managedShares(asset, lowerBidder), 0); + EXPECT_EQ(getBalance(earlierBidder), earlierBefore - 10); + EXPECT_EQ(getBalance(laterBidder), laterBefore); + EXPECT_EQ(getBalance(lowerBidder), lowerBefore); +} + TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) { ContractTestingNOST nostromo; @@ -1388,7 +1888,10 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); + nostromo.seedUser(bidder, 500); + const sint64 bidderBeforeBid = getBalance(bidder); + ASSERT_EQ(nostromo.placeBidWithFundedReward(bidder, createOutput.auctionId, 1, 120, 120).errorCode, + static_cast(NOST::EAuctionError::Success)); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); const auto rejectOutput = nostromo.resolvePendingStandardAuction(seller, createOutput.auctionId, false); @@ -1404,6 +1907,7 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction EXPECT_EQ(participant.participantData.allocatedQuantity, 0ULL); EXPECT_EQ(participant.participantData.escrowedAmount, 0ULL); EXPECT_EQ(nostromo.managedShares(asset, seller), 1); + EXPECT_EQ(getBalance(bidder), bidderBeforeBid); } { @@ -1535,6 +2039,99 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) } } +TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeRemainderAuction) +{ + const uint8 routeModes[] = {0, 1}; + for (uint32 routeIndex = 0; routeIndex < sizeof(routeModes) / sizeof(routeModes[0]); ++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, static_cast(NOST::EAuctionError::Success)); + + NOST::AuctionServiceFeeBreakdown expectedBatchBreakdown{}; + nostromo.calculateAuctionServiceFeeBreakdown(233ULL, expectedBatchBreakdown); + const sint64 expectedBatchDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedBatchBreakdown.shareholderDividendAmount); + 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.auctionId, 233); + EXPECT_EQ(batchCancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(batchCancelOutput.cancellationFee, 233ULL); + 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, expectedBatchBreakdown.managementFeeAmount); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedBatchBreakdown.developmentFeeAmount); + EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, + expectedBatchBreakdown.takeoverCoordinatorFeeAmount); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedBatchDividendPoolIncrease); + } + EXPECT_EQ(expectedBatchBreakdown.shareholderDividendAmount + expectedBatchBreakdown.managementFeeAmount + + expectedBatchBreakdown.developmentFeeAmount + expectedBatchBreakdown.takeoverCoordinatorFeeAmount, + 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::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(standardAsset, 1), 19, 19, 1)); + ASSERT_EQ(standardCreateOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + + NOST::AuctionServiceFeeBreakdown expectedSmallBreakdown{}; + smallFeeNostromo.calculateAuctionServiceFeeBreakdown(1ULL, expectedSmallBreakdown); + const sint64 expectedSmallDividendPoolIncrease = + smallFeeNostromo.expectedDividendPoolIncrease(expectedSmallBreakdown.shareholderDividendAmount); + const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); + const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); + + const auto standardCancelOutput = smallFeeNostromo.cancelAuction(standardSeller, standardCreateOutput.auctionId, 1); + EXPECT_EQ(standardCancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(standardCancelOutput.cancellationFee, 1ULL); + EXPECT_EQ(expectedSmallBreakdown.shareholderDividendAmount, 1ULL); + EXPECT_EQ(expectedSmallBreakdown.managementFeeAmount, 0ULL); + EXPECT_EQ(expectedSmallBreakdown.developmentFeeAmount, 0ULL); + EXPECT_EQ(expectedSmallBreakdown.takeoverCoordinatorFeeAmount, 0ULL); + 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, CancelAuctionRejectsAfterBidAuction) { ContractTestingNOST nostromo; @@ -1597,6 +2194,65 @@ TEST(ContractNostromoAuction, CancelAuctionRejectsInvalidCasesWithoutBidsAuction EXPECT_EQ(closed.errorCode, static_cast(NOST::EAuctionError::AuctionClosed)); } +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, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidder, finalizedCreateOutput.auctionId, 1, 10, 10).errorCode, + static_cast(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, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.cancelAuction(cancelledSeller, cancelledCreateOutput.auctionId, 1).errorCode, + static_cast(NOST::EAuctionError::Success)); + + const auto history = nostromo.getClosedAuctionHistory(); + EXPECT_EQ(history.totalEntries, 2ULL); + EXPECT_TRUE(containsAuctionId(history.auctionIds, history.totalEntries, finalizedCreateOutput.auctionId)); + EXPECT_TRUE(containsAuctionId(history.auctionIds, history.totalEntries, cancelledCreateOutput.auctionId)); + EXPECT_EQ(nostromo.getAuction(finalizedCreateOutput.auctionId).auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.getAuction(cancelledCreateOutput.auctionId).auction.core.status, NOST::EAuctionStatus::Cancelled); +} + +TEST(ContractNostromoAuction, ClosedAuctionHistoryGetterExposesRingBufferOverwriteAuction) +{ + ContractTestingNOST nostromo; + const id overwrittenAuctionId(22000, 22001, 22002, 22003); + const id latestAuctionId(23000, 23001, 23002, 23003); + + nostromo.stateData().closedAuctionHistory.set(0, overwrittenAuctionId); + nostromo.stateData().closedAuctionHistoryCounter = 1; + for (uint64 index = 1; index < NOST_AUCTION_HISTORY_NUM; ++index) + { + nostromo.stateData().closedAuctionHistory.set(index, id(24000 + index, 25000 + index, 26000 + index, 27000 + index)); + ++nostromo.stateData().closedAuctionHistoryCounter; + } + nostromo.stateData().closedAuctionHistory.set(0, latestAuctionId); + ++nostromo.stateData().closedAuctionHistoryCounter; + + const auto history = nostromo.getClosedAuctionHistory(); + EXPECT_EQ(history.totalEntries, NOST_AUCTION_HISTORY_NUM + 1ULL); + EXPECT_FALSE(containsAuctionId(history.auctionIds, history.totalEntries, overwrittenAuctionId)); + EXPECT_TRUE(containsAuctionId(history.auctionIds, history.totalEntries, latestAuctionId)); + EXPECT_EQ(history.auctionIds.get(0), latestAuctionId); +} + TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) { ContractTestingNOST nostromo; From 8dd4d329af29491436f103b33efa35a5dcbaefe8 Mon Sep 17 00:00:00 2001 From: N-010 Date: Mon, 18 May 2026 20:02:58 +0300 Subject: [PATCH 36/59] Reduce auction configuration limits: decrease lot items, allowed wallets, and required access assets for improved efficiency. --- src/contracts/Nostromo.h | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index b536fd9b4..f4721c71f 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -12,9 +12,9 @@ constexpr uint64 NOST_AUCTION_NUM = 2048; constexpr uint64 NOST_AUCTION_HISTORY_NUM = 1024; constexpr uint64 NOST_AUCTION_METADATA_CID_LENGTH = 64; constexpr uint64 NOST_AUCTION_PARTICIPANT_NUM = 4096; -constexpr uint64 NOST_AUCTION_LOT_ITEM_NUM = 64; -constexpr uint64 NOST_AUCTION_ALLOWED_WALLET_NUM = 128; -constexpr uint64 NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM = 16; +constexpr uint64 NOST_AUCTION_LOT_ITEM_NUM = 8; +constexpr uint64 NOST_AUCTION_ALLOWED_WALLET_NUM = 8; +constexpr uint64 NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM = 4; constexpr uint32 NOST_AUCTION_MAX_DURATION_DAYS = 30; constexpr sint64 NOST_DEFAULT_PRIVATE_AUCTION_FEE = 50000000LL; constexpr uint64 NOST_DEFAULT_AUCTION_CANCELLATION_FEE_BP = 1000ULL; @@ -2554,7 +2554,7 @@ struct NOST : public ContractBase if (!resolveBatchAuctionCreateParams(locals.analyzeAuctionLotOutput.lotItemCount, locals.analyzeAuctionLotOutput.totalEscrowQuantity, input.minimumPurchaseQuantity, locals.resolvedQuantityForSale, - locals.resolvedMinimumPurchaseQuantity, input.buyNowPrice)) + locals.resolvedMinimumPurchaseQuantity)) { if (qpi.invocationReward() > 0) { @@ -3270,7 +3270,7 @@ struct NOST : public ContractBase { return a > b ? a : b; } - static bool resolveBatchAuctionCreateParams(uint64 lotItemCount, uint64 totalEscrowQuantity, uint64 minimumPurchaseQuantity, + static bool resolveBatchAuctionCreateParams(uint64 lotItemCount, uint64 totalEscrowQuantity, uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPrice) { quantityForSale = 0; @@ -3280,7 +3280,6 @@ struct NOST : public ContractBase return false; } quantityForSale = totalEscrowQuantity; - (void)minimumPurchaseQuantity; resolvedMinimumPurchaseQuantity = 0; return true; } From 0c6f4c9fc7bb737f71bacaecb9b276bbca3f1260 Mon Sep 17 00:00:00 2001 From: N-010 Date: Mon, 25 May 2026 21:27:06 +0300 Subject: [PATCH 37/59] Replace `auctionId` with `auctionIndex` across `ContractNostromoAuction` methods for consistency and clarity. Introduce expanded auction getter methods. Update related tests and assertions. --- src/contracts/Nostromo.h | 981 ++++++++++++++++++++++++++++++------- test/contract_nostromo.cpp | 766 ++++++++++++++++++----------- 2 files changed, 1303 insertions(+), 444 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index f4721c71f..33f0fe617 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -12,6 +12,7 @@ constexpr uint64 NOST_AUCTION_NUM = 2048; constexpr uint64 NOST_AUCTION_HISTORY_NUM = 1024; constexpr uint64 NOST_AUCTION_METADATA_CID_LENGTH = 64; constexpr uint64 NOST_AUCTION_PARTICIPANT_NUM = 4096; +constexpr uint64 NOST_AUCTION_GETTER_PAGE_SIZE = 64; constexpr uint64 NOST_AUCTION_LOT_ITEM_NUM = 8; constexpr uint64 NOST_AUCTION_ALLOWED_WALLET_NUM = 8; constexpr uint64 NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM = 4; @@ -89,10 +90,10 @@ struct NOST : public ContractBase struct AuctionParticipantKey { - id auctionId; + uint64 auctionIndex; id participant; - bool operator==(const AuctionParticipantKey& rhs) const { return auctionId == rhs.auctionId && participant == rhs.participant; } + bool operator==(const AuctionParticipantKey& rhs) const { return auctionIndex == rhs.auctionIndex && participant == rhs.participant; } }; /** @@ -151,9 +152,6 @@ struct NOST : public ContractBase /** @brief Lowercase base32 CIDv1 stored in Pinata for the auction name and description metadata. */ Array metadataIpfsCid; - /** @brief Unique identifier of the auction created in the Auction House. */ - id auctionId; - /** @brief Wallet that created the auction and offers the lot for sale. */ id seller; @@ -206,6 +204,9 @@ struct NOST : public ContractBase /** @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 Auction House mode: Batch Auction or Standard Auction. */ EAuctionType type; @@ -317,13 +318,16 @@ struct NOST : public ContractBase id takeoverCoordinator; - /** @brief Circular buffer with identifiers of finalized and cancelled auctions. */ - Array closedAuctionHistory; + /** @brief Total number of auctions ever created; also the next auction index. */ + uint64 totalAuctionsCreated; + + /** @brief Circular buffer with indices of finalized and cancelled auctions. */ + Array closedAuctionHistory; /** @brief Monotonic insertion counter for `closedAuctionHistory`. */ uint64 closedAuctionHistoryCounter; - HashMap auctionList; + HashMap auctionList; HashMap participants; }; @@ -371,18 +375,18 @@ struct NOST : public ContractBase /** @brief Result of auction creation. */ struct CreateAuction_output { - /** @brief Identifier assigned to the new auction when creation succeeds. */ - id auctionId; + /** @brief Monotonic index assigned to the new auction when creation succeeds. */ + uint64 auctionIndex; /** @brief Result code describing whether the auction creation succeeded. */ - uint8 errorCode; + EAuctionError errorCode; }; /** @brief Input payload used to place a bid in a Batch Auction or Standard Auction. */ struct PlaceBid_input { - /** @brief Identifier of the target auction. */ - id auctionId; + /** @brief Monotonic index of the target auction. */ + uint64 auctionIndex; /** @brief Requested quantity for a batch auction; ignored for a standard auction because the whole lot is sold as one unit. */ uint64 quantity; @@ -401,14 +405,14 @@ struct NOST : public ContractBase uint64 refundedAmount; /** @brief Result code describing whether the bid placement succeeded. */ - uint8 errorCode; + EAuctionError errorCode; }; /** @brief Input payload used to cancel an active auction. */ struct CancelAuction_input { - /** @brief Identifier of the auction that the seller wants to cancel. */ - id auctionId; + /** @brief Monotonic index of the auction that the seller wants to cancel. */ + uint64 auctionIndex; }; /** @brief Result of an auction cancellation request. */ @@ -421,14 +425,14 @@ struct NOST : public ContractBase uint64 cancellationFee; /** @brief Result code describing whether the cancellation succeeded. */ - uint8 errorCode; + EAuctionError errorCode; }; /** @brief Input payload used by the seller to accept or reject a pending standard auction result. */ struct ResolvePendingStandardAuction_input { - /** @brief Identifier of the standard auction awaiting the seller decision. */ - id auctionId; + /** @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; @@ -441,7 +445,7 @@ struct NOST : public ContractBase uint64 refundedAmount; /** @brief Result code describing whether the seller decision was applied. */ - uint8 errorCode; + EAuctionError errorCode; }; /** @brief Input payload used by the takeover coordinator to overwrite the full auction fee configuration. */ @@ -481,7 +485,7 @@ struct NOST : public ContractBase struct SetAuctionFees_output { /** @brief Result code describing whether the fee update succeeded. */ - uint8 errorCode; + EAuctionError errorCode; }; /** @brief Input payload used by management to update every fee except takeover coordinator-specific splits. */ @@ -515,7 +519,7 @@ struct NOST : public ContractBase struct SetAuctionFeesByManagement_output { /** @brief Result code describing whether the fee update succeeded. */ - uint8 errorCode; + EAuctionError errorCode; }; /** @brief Input payload used by the takeover coordinator to appoint a new management wallet. */ @@ -528,18 +532,18 @@ struct NOST : public ContractBase struct SetManagement_output { /** @brief Result code describing whether the management update succeeded. */ - uint8 errorCode; + EAuctionError errorCode; }; /** @brief Input payload used to fetch one auction from storage. */ - struct GetAuction_input + struct GetAuctionByIndex_input { - /** @brief Identifier of the auction to read. */ - id auctionId; + /** @brief Monotonic index of the auction to read. */ + uint64 auctionIndex; }; /** @brief Auction data returned by the read-only auction getter. */ - struct GetAuction_output + struct GetAuctionByIndex_output { /** @brief Serializable auction data stored for the requested auction. */ AuctionView auction; @@ -551,8 +555,8 @@ struct NOST : public ContractBase /** @brief Input payload used to fetch one participant record from an auction. */ struct GetAuctionParticipant_input { - /** @brief Identifier of the auction that owns the participant record. */ - id auctionId; + /** @brief Monotonic index of the auction that owns the participant record. */ + uint64 auctionIndex; /** @brief Wallet whose participant record should be returned. */ id participant; @@ -684,8 +688,8 @@ struct NOST : public ContractBase struct GetClosedAuctionHistory_output { - /** @brief Ring buffer of auction identifiers recorded after finalization or cancellation. */ - Array auctionIds; + /** @brief Ring buffer of auction indices recorded after finalization or cancellation. */ + Array auctionIndices; /** @brief Total number of history writes since initialization. */ uint64 totalEntries; @@ -700,6 +704,194 @@ struct NOST : public ContractBase 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; + uint32 qxTransferFee; + uint8 routeAllFeesToDevelopment; + uint8 isAuctionTimerPaused; + uint8 isPostBeginEpochPauseArmed; + }; + + 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 Internal input used to validate an auction lot and resolve its total escrow quantity. */ struct AnalyzeAuctionLot_input { @@ -768,7 +960,19 @@ struct NOST : public ContractBase uint64 requiredAccessAssetIndex; }; - struct GetAuction_locals + struct NostromoProcedureLog + { + id actor; + sint64 amount; + uint64 auctionIndex; + uint32 contractIndex; + uint8 procedure; + EAuctionError errorCode; + + sint8 _terminator; + }; + + struct GetAuctionByIndex_locals { AuctionData auction; Asset requiredAccessAsset; @@ -777,11 +981,38 @@ struct NOST : public ContractBase sint64 allowedBidderWalletSetIndex; }; + struct GetterScan_locals + { + AuctionData auction; + AuctionParticipantKey participantKey; + AuctionParticipantData participantData; + AuctionSummary auctionSummary; + ParticipantSummary participantSummary; + UserParticipationSummary userParticipationSummary; + uint64 auctionIndex; + uint64 boundedLimit; + uint64 metadataIndex; + uint64 requestedIndex; + sint64 participantMapIndex; + uint8 metadataMatches; + }; + + using GetContractStats_locals = GetterScan_locals; + using GetAuctionSummaries_locals = GetterScan_locals; + using GetActiveAuctionIndices_locals = GetterScan_locals; + using GetAuctionsBySeller_locals = GetterScan_locals; + using GetAuctionByMetadataCid_locals = GetterScan_locals; + using GetAuctionSummariesByIndexBatch_locals = GetterScan_locals; + using GetAuctionParticipants_locals = GetterScan_locals; + using GetUserParticipations_locals = GetterScan_locals; + using GetAuctionCountBySeller_locals = GetterScan_locals; + using GetAuctionAtCreationSnapshot_locals = GetterScan_locals; + /** @brief Internal input used to verify whether the invocator owns at least one required private access asset. */ struct HasRequiredAccessAsset_input { - /** @brief Identifier of the auction whose private asset-based access rules should be evaluated. */ - id auctionId; + /** @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. */ @@ -805,8 +1036,8 @@ struct NOST : public ContractBase /** @brief Timestamp used as the auction settlement time. */ DateAndTime currentDate; - /** @brief Identifier of the batch auction to finalize. */ - id auctionId; + /** @brief Monotonic index of the batch auction to finalize. */ + uint64 auctionIndex; }; /** @brief Internal output returned after batch auction finalization. */ @@ -822,8 +1053,8 @@ struct NOST : public ContractBase /** @brief Timestamp used as the auction settlement time. */ DateAndTime currentDate; - /** @brief Identifier of the standard auction to finalize. */ - id auctionId; + /** @brief Monotonic index of the standard auction to finalize. */ + uint64 auctionIndex; }; /** @brief Internal output returned after standard auction finalization. */ @@ -839,8 +1070,8 @@ struct NOST : public ContractBase /** @brief Timestamp used as the auction settlement time. */ DateAndTime currentDate; - /** @brief Identifier of the pending standard auction to reject. */ - id auctionId; + /** @brief Monotonic index of the pending standard auction to reject. */ + uint64 auctionIndex; }; /** @brief Internal output returned after rejecting a pending standard auction. */ @@ -965,8 +1196,8 @@ struct NOST : public ContractBase /** @brief Internal input used to process a batch auction bid after the common PlaceBid checks succeed. */ struct ProcessBatchBid_input { - /** @brief Identifier of the target batch auction. */ - id auctionId; + /** @brief Monotonic index of the target batch auction. */ + uint64 auctionIndex; /** @brief Quantity requested by the bidder in the batch auction. */ uint64 effectiveQuantity; @@ -984,8 +1215,8 @@ struct NOST : public ContractBase /** @brief Internal input used to refresh the cached highest bid fields of one batch auction. */ struct RecomputeBatchHighestBid_input { - /** @brief Identifier of the batch auction whose cached top bid must be rebuilt. */ - id auctionId; + /** @brief Monotonic index of the batch auction whose cached top bid must be rebuilt. */ + uint64 auctionIndex; }; using RecomputeBatchHighestBid_output = NoData; @@ -1000,7 +1231,7 @@ struct NOST : public ContractBase uint64 refundedAmount; /** @brief Result code describing whether the batch bid processing succeeded. */ - uint8 errorCode; + EAuctionError errorCode; /** @brief Flag indicating whether batch bid processing completed successfully. */ uint8 success; @@ -1033,8 +1264,8 @@ struct NOST : public ContractBase /** @brief Internal input used to process a standard auction bid after the common PlaceBid checks succeed. */ struct ProcessStandardBid_input { - /** @brief Identifier of the target standard auction. */ - id auctionId; + /** @brief Monotonic index of the target standard auction. */ + uint64 auctionIndex; /** @brief Total amount the bidder commits for the standard auction lot. */ uint64 bidAmount; @@ -1056,7 +1287,7 @@ struct NOST : public ContractBase uint64 refundedAmount; /** @brief Result code describing whether the standard bid processing succeeded. */ - uint8 errorCode; + EAuctionError errorCode; /** @brief Flag indicating whether standard bid processing completed successfully. */ uint8 success; @@ -1224,6 +1455,7 @@ struct NOST : public ContractBase struct CreateAuction_locals { AuctionData auction; + NostromoProcedureLog log; IsAuctionInteractionPaused_input isAuctionInteractionPausedInput; IsAuctionInteractionPaused_output isAuctionInteractionPausedOutput; ValidateMetadataCid_input validateMetadataCidInput; @@ -1252,6 +1484,7 @@ struct NOST : public ContractBase struct PlaceBid_locals { AuctionData auction; + NostromoProcedureLog log; IsAuctionInteractionPaused_input isAuctionInteractionPausedInput; IsAuctionInteractionPaused_output isAuctionInteractionPausedOutput; HasRequiredAccessAsset_input hasRequiredAccessAssetInput; @@ -1270,6 +1503,7 @@ struct NOST : public ContractBase AuctionData auction; AuctionParticipantData participantData; AuctionParticipantKey participantKey; + NostromoProcedureLog log; RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; DistributeAuctionServiceFee_input distributeAuctionServiceFeeInput; @@ -1283,6 +1517,8 @@ struct NOST : public ContractBase { AuctionData auction; DateAndTime currentDate; + NostromoProcedureLog log; + IsAuctionInteractionPaused_input isAuctionInteractionPausedInput; IsAuctionInteractionPaused_output isAuctionInteractionPausedOutput; FinalizeStandardAuction_input finalizeStandardAuctionInput; @@ -1334,12 +1570,29 @@ struct NOST : public ContractBase 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; + }; + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() { REGISTER_USER_PROCEDURE(CreateAuction, 1); @@ -1351,13 +1604,24 @@ struct NOST : public ContractBase REGISTER_USER_PROCEDURE(SetAuctionFeesByManagement, 7); REGISTER_USER_PROCEDURE(SetManagement, 8); - REGISTER_USER_FUNCTION(GetAuction, 1); + REGISTER_USER_FUNCTION(GetAuctionByIndex, 1); REGISTER_USER_FUNCTION(GetAuctionParticipant, 2); REGISTER_USER_FUNCTION(GetTicksBeforeAuctionLaunch, 3); REGISTER_USER_FUNCTION(GetAuctionFees, 4); REGISTER_USER_FUNCTION(GetFeeRecipients, 5); REGISTER_USER_FUNCTION(GetClosedAuctionHistory, 6); REGISTER_USER_FUNCTION(GetRouteAllFeesToDevelopment, 7); + REGISTER_USER_FUNCTION(GetContractStats, 8); + REGISTER_USER_FUNCTION(GetAuctionSummaries, 9); + REGISTER_USER_FUNCTION(GetActiveAuctionIndices, 10); + REGISTER_USER_FUNCTION(GetAuctionsBySeller, 11); + REGISTER_USER_FUNCTION(GetAuctionByMetadataCid, 12); + REGISTER_USER_FUNCTION(GetAuctionSummariesByIndexBatch, 13); + REGISTER_USER_FUNCTION(GetAuctionParticipants, 14); + REGISTER_USER_FUNCTION(GetUserParticipations, 15); + REGISTER_USER_FUNCTION(GetLatestAuctionIndex, 16); + REGISTER_USER_FUNCTION(GetAuctionCountBySeller, 17); + REGISTER_USER_FUNCTION(GetAuctionAtCreationSnapshot, 18); } INITIALIZE() @@ -1475,14 +1739,14 @@ struct NOST : public ContractBase switch (locals.auction.core.type) { case EAuctionType::Batch: - locals.finalizeBatchAuctionInput.auctionId = locals.auction.core.auctionId; + locals.finalizeBatchAuctionInput.auctionIndex = locals.auction.core.auctionIndex; locals.finalizeBatchAuctionInput.currentDate = locals.currentDate; CALL(FinalizeBatchAuction, locals.finalizeBatchAuctionInput, locals.finalizeBatchAuctionOutput); break; case EAuctionType::Standard: if (locals.auction.core.highestBidAmount == 0 || locals.auction.core.highestBidPrice >= locals.auction.core.salePrice) { - locals.finalizeStandardAuctionInput.auctionId = locals.auction.core.auctionId; + locals.finalizeStandardAuctionInput.auctionIndex = locals.auction.core.auctionIndex; locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); } @@ -1491,7 +1755,7 @@ struct NOST : public ContractBase 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.auctionId, locals.auction); + state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); } break; default: break; @@ -1501,7 +1765,7 @@ struct NOST : public ContractBase else if (locals.auction.core.status == EAuctionStatus::PendingSellerDecision && locals.auction.core.sellerDecisionDeadline <= locals.currentDate) { - locals.finalizeStandardAuctionInput.auctionId = locals.auction.core.auctionId; + locals.finalizeStandardAuctionInput.auctionIndex = locals.auction.core.auctionIndex; locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); } @@ -1675,12 +1939,12 @@ struct NOST : public ContractBase 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.auctionId, locals.auction); + 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.auctionId, locals.auction); + state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); } locals.auctionIndex = state.get().auctionList.nextElementIndex(locals.auctionIndex); } @@ -1826,7 +2090,7 @@ struct NOST : public ContractBase PRIVATE_FUNCTION_WITH_LOCALS(HasRequiredAccessAsset) { output.hasRequiredAccessAsset = 0; - if (!state.get().auctionList.get(input.auctionId, locals.auction)) + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { return; } @@ -1850,7 +2114,7 @@ struct NOST : public ContractBase { locals.bestParticipantFound = 0; - if (!state.get().auctionList.get(input.auctionId, locals.auction)) + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { return; } @@ -1864,7 +2128,7 @@ struct NOST : public ContractBase locals.participantIndex = state.get().participants.nextElementIndex(locals.participantIndex)) { locals.participantKey = state.get().participants.key(locals.participantIndex); - if (locals.participantKey.auctionId != input.auctionId) + if (locals.participantKey.auctionIndex != input.auctionIndex) { continue; } @@ -1900,41 +2164,41 @@ struct NOST : public ContractBase locals.auction.core.highestBidder = NULL_ID; } - state.mut().auctionList.replace(locals.auction.core.auctionId, locals.auction); + state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); } PRIVATE_PROCEDURE_WITH_LOCALS(ProcessBatchBid) { output.escrowedAmount = 0; output.refundedAmount = 0; - output.errorCode = static_cast(EAuctionError::Success); + output.errorCode = EAuctionError::Success; output.success = 0; - if (!state.get().auctionList.get(input.auctionId, locals.auction)) + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { - output.errorCode = static_cast(EAuctionError::AuctionNotFound); + output.errorCode = EAuctionError::AuctionNotFound; return; } if (input.effectiveQuantity == 0 || input.bidAmount == 0) { - output.errorCode = static_cast(EAuctionError::InvalidInput); + output.errorCode = EAuctionError::InvalidInput; return; } if (input.bidAmount < locals.auction.core.salePrice) { - output.errorCode = static_cast(EAuctionError::BidTooLow); + output.errorCode = EAuctionError::BidTooLow; return; } locals.requiredEscrow = smul(input.effectiveQuantity, input.bidAmount); if (static_cast(qpi.invocationReward()) < locals.requiredEscrow) { - output.errorCode = static_cast(EAuctionError::InsufficientFunds); + output.errorCode = EAuctionError::InsufficientFunds; return; } - locals.participantKey = {input.auctionId, qpi.invocator()}; + locals.participantKey = {input.auctionIndex, qpi.invocator()}; locals.participantExists = state.get().participants.get(locals.participantKey, locals.participantData); locals.previousEscrow = locals.participantExists ? locals.participantData.escrowedAmount : 0; locals.mustRecomputeHighestBid = locals.participantExists && locals.auction.core.highestBidder == qpi.invocator() && @@ -1964,13 +2228,13 @@ struct NOST : public ContractBase if (state.mut().participants.set(locals.participantKey, locals.participantData) == NULL_INDEX) { - output.errorCode = static_cast(EAuctionError::StorageFull); + output.errorCode = EAuctionError::StorageFull; return; } - state.mut().auctionList.replace(input.auctionId, locals.auction); + state.mut().auctionList.replace(input.auctionIndex, locals.auction); if (locals.mustRecomputeHighestBid) { - locals.recomputeBatchHighestBidInput.auctionId = input.auctionId; + locals.recomputeBatchHighestBidInput.auctionIndex = input.auctionIndex; CALL(RecomputeBatchHighestBid, locals.recomputeBatchHighestBidInput, locals.recomputeBatchHighestBidOutput); } @@ -1993,27 +2257,27 @@ struct NOST : public ContractBase { output.escrowedAmount = 0; output.refundedAmount = 0; - output.errorCode = static_cast(EAuctionError::Success); + output.errorCode = EAuctionError::Success; output.success = 0; locals.highestBidderExists = 0; locals.finalizeImmediately = 0; - if (!state.get().auctionList.get(input.auctionId, locals.auction)) + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { - output.errorCode = static_cast(EAuctionError::AuctionNotFound); + output.errorCode = EAuctionError::AuctionNotFound; return; } if (locals.auction.core.quantityForSale == 0 || locals.auction.core.quantityForSale < locals.auction.core.minimumPurchaseQuantity || input.bidAmount == 0) { - output.errorCode = static_cast(EAuctionError::InvalidInput); + output.errorCode = EAuctionError::InvalidInput; return; } locals.requiredEscrow = input.bidAmount; if (static_cast(qpi.invocationReward()) < locals.requiredEscrow) { - output.errorCode = static_cast(EAuctionError::InsufficientFunds); + output.errorCode = EAuctionError::InsufficientFunds; return; } @@ -2021,22 +2285,22 @@ struct NOST : public ContractBase { if (input.bidAmount < locals.auction.core.initialPrice) { - output.errorCode = static_cast(EAuctionError::BidTooLow); + output.errorCode = EAuctionError::BidTooLow; return; } } else if (input.bidAmount < sadd(locals.auction.core.highestBidPrice, locals.auction.core.minimumBidIncrement)) { - output.errorCode = static_cast(EAuctionError::BidTooLow); + output.errorCode = EAuctionError::BidTooLow; return; } - locals.participantKey = {input.auctionId, qpi.invocator()}; + locals.participantKey = {input.auctionIndex, qpi.invocator()}; locals.participantExists = state.get().participants.get(locals.participantKey, locals.participantData); locals.previousEscrow = locals.participantExists ? locals.participantData.escrowedAmount : 0; if (!locals.participantExists && state.get().participants.population() >= state.get().participants.capacity()) { - output.errorCode = static_cast(EAuctionError::StorageFull); + output.errorCode = EAuctionError::StorageFull; return; } @@ -2050,7 +2314,7 @@ struct NOST : public ContractBase if (!isZero(locals.auction.core.highestBidder)) { - locals.highestBidderKey = {locals.auction.core.auctionId, locals.auction.core.highestBidder}; + locals.highestBidderKey = {locals.auction.core.auctionIndex, locals.auction.core.highestBidder}; locals.highestBidderExists = state.get().participants.get(locals.highestBidderKey, locals.previousHighestBidderData); } if (locals.highestBidderExists && locals.previousHighestBidderData.participant != qpi.invocator()) @@ -2080,10 +2344,10 @@ struct NOST : public ContractBase if (state.mut().participants.set(locals.participantKey, locals.participantData) == NULL_INDEX) { - output.errorCode = static_cast(EAuctionError::StorageFull); + output.errorCode = EAuctionError::StorageFull; return; } - state.mut().auctionList.replace(input.auctionId, locals.auction); + state.mut().auctionList.replace(input.auctionIndex, locals.auction); if (locals.previousEscrow > 0) { @@ -2101,7 +2365,7 @@ struct NOST : public ContractBase if (locals.finalizeImmediately) { - locals.finalizeStandardAuctionInput.auctionId = input.auctionId; + locals.finalizeStandardAuctionInput.auctionIndex = input.auctionIndex; locals.finalizeStandardAuctionInput.currentDate = input.currentDate; CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); } @@ -2193,7 +2457,7 @@ struct NOST : public ContractBase locals.totalGrossAmount = 0; // Abort if the auction no longer exists or is no longer an active batch auction. - if (!state.get().auctionList.get(input.auctionId, locals.auction)) + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { return; } @@ -2229,7 +2493,7 @@ struct NOST : public ContractBase while (locals.participantIndex != NULL_INDEX) { locals.participantKey = state.get().participants.key(locals.participantIndex); - if (locals.participantKey.auctionId == input.auctionId) + if (locals.participantKey.auctionIndex == input.auctionIndex) { locals.participantData = state.get().participants.value(locals.participantIndex); if (locals.participantData.escrowedAmount > 0) @@ -2289,7 +2553,7 @@ struct NOST : public ContractBase while (locals.participantIndex != NULL_INDEX) { locals.participantKey = state.get().participants.key(locals.participantIndex); - if (locals.participantKey.auctionId == input.auctionId) + if (locals.participantKey.auctionIndex == input.auctionIndex) { locals.participantData = state.get().participants.value(locals.participantIndex); if (locals.participantData.escrowedAmount > 0) @@ -2323,8 +2587,8 @@ struct NOST : public ContractBase locals.auction.core.allocatedQuantity = locals.soldQuantity; locals.auction.core.status = EAuctionStatus::Finalized; locals.auction.core.settledAt = input.currentDate; - state.mut().auctionList.replace(locals.auction.core.auctionId, locals.auction); - addClosedAuctionToHistory(state, locals.auction.core.auctionId); + state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); + addClosedAuctionToHistory(state, locals.auction.core.auctionIndex); output.success = 1; } @@ -2334,7 +2598,7 @@ struct NOST : public ContractBase locals.highestBidderExists = 0; locals.lotSold = 0; - if (!state.get().auctionList.get(input.auctionId, locals.auction)) + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { return; } @@ -2346,7 +2610,7 @@ struct NOST : public ContractBase if (!isZero(locals.auction.core.highestBidder)) { - locals.highestBidderKey = {locals.auction.core.auctionId, locals.auction.core.highestBidder}; + locals.highestBidderKey = {locals.auction.core.auctionIndex, locals.auction.core.highestBidder}; locals.highestBidderExists = state.get().participants.get(locals.highestBidderKey, locals.highestBidderData); } @@ -2387,8 +2651,8 @@ struct NOST : public ContractBase locals.auction.core.highestBidQuantity = 0; locals.auction.core.highestBidder = NULL_ID; } - state.mut().auctionList.replace(locals.auction.core.auctionId, locals.auction); - addClosedAuctionToHistory(state, locals.auction.core.auctionId); + state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); + addClosedAuctionToHistory(state, locals.auction.core.auctionIndex); output.success = 1; } @@ -2398,7 +2662,7 @@ struct NOST : public ContractBase output.success = 0; locals.highestBidderExists = 0; - if (!state.get().auctionList.get(input.auctionId, locals.auction)) + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { return; } @@ -2410,7 +2674,7 @@ struct NOST : public ContractBase if (!isZero(locals.auction.core.highestBidder)) { - locals.highestBidderKey = {locals.auction.core.auctionId, locals.auction.core.highestBidder}; + locals.highestBidderKey = {locals.auction.core.auctionIndex, locals.auction.core.highestBidder}; locals.highestBidderExists = state.get().participants.get(locals.highestBidderKey, locals.highestBidderData); } @@ -2435,8 +2699,8 @@ struct NOST : public ContractBase locals.auction.core.highestBidder = NULL_ID; locals.auction.core.status = EAuctionStatus::Finalized; locals.auction.core.settledAt = input.currentDate; - state.mut().auctionList.replace(locals.auction.core.auctionId, locals.auction); - addClosedAuctionToHistory(state, locals.auction.core.auctionId); + state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); + addClosedAuctionToHistory(state, locals.auction.core.auctionIndex); output.success = 1; } @@ -2481,7 +2745,7 @@ struct NOST : public ContractBase */ PUBLIC_PROCEDURE_WITH_LOCALS(CreateAuction) { - output.errorCode = static_cast(EAuctionError::InvalidInput); + output.errorCode = EAuctionError::InvalidInput; CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); if (locals.isAuctionInteractionPausedOutput.isPaused) @@ -2490,7 +2754,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::AuctionPaused); + output.errorCode = EAuctionError::AuctionPaused; + setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + LOG_INFO(locals.log); return; } @@ -2500,7 +2766,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::InvalidAuctionType); + output.errorCode = EAuctionError::InvalidAuctionType; + setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + LOG_INFO(locals.log); return; } @@ -2510,7 +2778,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::InvalidVisibility); + output.errorCode = EAuctionError::InvalidVisibility; + setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + LOG_INFO(locals.log); return; } @@ -2520,7 +2790,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::StorageFull); + output.errorCode = EAuctionError::StorageFull; + setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + LOG_INFO(locals.log); return; } @@ -2532,6 +2804,8 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + LOG_INFO(locals.log); return; } @@ -2544,6 +2818,8 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + LOG_INFO(locals.log); return; } locals.resolvedQuantityForSale = 0; @@ -2553,13 +2829,14 @@ struct NOST : public ContractBase case EAuctionType::Batch: if (!resolveBatchAuctionCreateParams(locals.analyzeAuctionLotOutput.lotItemCount, locals.analyzeAuctionLotOutput.totalEscrowQuantity, - input.minimumPurchaseQuantity, locals.resolvedQuantityForSale, - locals.resolvedMinimumPurchaseQuantity)) + locals.resolvedQuantityForSale, locals.resolvedMinimumPurchaseQuantity, input.buyNowPrice)) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + LOG_INFO(locals.log); return; } break; @@ -2572,6 +2849,8 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + LOG_INFO(locals.log); return; } break; @@ -2580,7 +2859,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::InvalidAuctionType); + output.errorCode = EAuctionError::InvalidAuctionType; + setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + LOG_INFO(locals.log); return; } @@ -2596,6 +2877,8 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + LOG_INFO(locals.log); return; } @@ -2606,7 +2889,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::InsufficientFunds); + output.errorCode = EAuctionError::InsufficientFunds; + setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + LOG_INFO(locals.log); return; } @@ -2618,7 +2903,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::InsufficientAssetBalance); + output.errorCode = EAuctionError::InsufficientAssetBalance; + setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + LOG_INFO(locals.log); return; } @@ -2630,11 +2917,13 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::InsufficientAssetBalance); + output.errorCode = EAuctionError::InsufficientAssetBalance; + setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + LOG_INFO(locals.log); return; } - locals.auction.core.auctionId = id::randomValue(); + 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; @@ -2666,7 +2955,7 @@ struct NOST : public ContractBase locals.auction.core.visibility = static_cast(input.auctionVisibility); locals.auction.core.status = EAuctionStatus::Active; - if (state.mut().auctionList.set(locals.auction.core.auctionId, locals.auction) == NULL_INDEX) + if (state.mut().auctionList.set(locals.auction.core.auctionIndex, locals.auction) == NULL_INDEX) { locals.rollbackAuctionLotAssetsInput.auctionLotItems = input.auctionLotItems; locals.rollbackAuctionLotAssetsInput.recipient = qpi.invocator(); @@ -2675,7 +2964,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::StorageFull); + output.errorCode = EAuctionError::StorageFull; + setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + LOG_INFO(locals.log); return; } @@ -2687,8 +2978,11 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.requiredFee); } - output.auctionId = locals.auction.core.auctionId; - output.errorCode = static_cast(EAuctionError::Success); + output.auctionIndex = locals.auction.core.auctionIndex; + state.mut().totalAuctionsCreated = sadd(state.get().totalAuctionsCreated, 1ULL); + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + LOG_INFO(locals.log); } /** @@ -2698,7 +2992,7 @@ struct NOST : public ContractBase */ PUBLIC_PROCEDURE_WITH_LOCALS(PlaceBid) { - output.errorCode = static_cast(EAuctionError::InvalidInput); + output.errorCode = EAuctionError::InvalidInput; CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); if (locals.isAuctionInteractionPausedOutput.isPaused) @@ -2707,17 +3001,21 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::AuctionPaused); + output.errorCode = EAuctionError::AuctionPaused; + setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + LOG_INFO(locals.log); return; } - if (!state.get().auctionList.get(input.auctionId, locals.auction)) + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::AuctionNotFound); + output.errorCode = EAuctionError::AuctionNotFound; + setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + LOG_INFO(locals.log); return; } @@ -2727,7 +3025,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::AuctionClosed); + output.errorCode = EAuctionError::AuctionClosed; + setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + LOG_INFO(locals.log); return; } @@ -2737,7 +3037,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::Forbidden); + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + LOG_INFO(locals.log); return; } @@ -2749,7 +3051,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::AuctionClosed); + output.errorCode = EAuctionError::AuctionClosed; + setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + LOG_INFO(locals.log); return; } @@ -2757,7 +3061,7 @@ struct NOST : public ContractBase { if (locals.auction.requiredAccessAssets.population() > 0) { - locals.hasRequiredAccessAssetInput.auctionId = input.auctionId; + locals.hasRequiredAccessAssetInput.auctionIndex = input.auctionIndex; CALL(HasRequiredAccessAsset, locals.hasRequiredAccessAssetInput, locals.hasRequiredAccessAssetOutput); locals.hasAccess = locals.hasRequiredAccessAssetOutput.hasRequiredAccessAsset; } @@ -2772,7 +3076,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::PrivateAuctionAccessDenied); + output.errorCode = EAuctionError::PrivateAuctionAccessDenied; + setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + LOG_INFO(locals.log); return; } } @@ -2780,7 +3086,7 @@ struct NOST : public ContractBase switch (locals.auction.core.type) { case EAuctionType::Batch: - locals.processBatchBidInput.auctionId = input.auctionId; + locals.processBatchBidInput.auctionIndex = input.auctionIndex; locals.processBatchBidInput.effectiveQuantity = input.quantity; locals.processBatchBidInput.bidAmount = input.bidAmount; locals.processBatchBidInput.currentDate = locals.currentDate; @@ -2793,13 +3099,15 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = locals.processBatchBidOutput.errorCode; + setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + LOG_INFO(locals.log); return; } output.refundedAmount = sadd(output.refundedAmount, locals.processBatchBidOutput.refundedAmount); output.escrowedAmount = locals.processBatchBidOutput.escrowedAmount; break; case EAuctionType::Standard: - locals.processStandardBidInput.auctionId = input.auctionId; + locals.processStandardBidInput.auctionIndex = input.auctionIndex; locals.processStandardBidInput.bidAmount = input.bidAmount; locals.processStandardBidInput.currentDate = locals.currentDate; locals.processStandardBidInput.elapsedSeconds = locals.elapsedSeconds; @@ -2811,6 +3119,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = locals.processStandardBidOutput.errorCode; + setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + LOG_INFO(locals.log); return; } output.refundedAmount = sadd(output.refundedAmount, locals.processStandardBidOutput.refundedAmount); @@ -2821,10 +3131,14 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::InvalidAuctionType); + output.errorCode = EAuctionError::InvalidAuctionType; + setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + LOG_INFO(locals.log); return; } - output.errorCode = static_cast(EAuctionError::Success); + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + LOG_INFO(locals.log); } /** @@ -2837,15 +3151,17 @@ struct NOST : public ContractBase { output.refundedAmount = 0; output.cancellationFee = 0; - output.errorCode = static_cast(EAuctionError::InvalidInput); + output.errorCode = EAuctionError::InvalidInput; - if (!state.get().auctionList.get(input.auctionId, locals.auction)) + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::AuctionNotFound); + output.errorCode = EAuctionError::AuctionNotFound; + setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); + LOG_INFO(locals.log); return; } @@ -2855,7 +3171,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::AuctionClosed); + output.errorCode = EAuctionError::AuctionClosed; + setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); + LOG_INFO(locals.log); return; } @@ -2865,7 +3183,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::Forbidden); + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); + LOG_INFO(locals.log); return; } @@ -2875,7 +3195,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::Forbidden); + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); + LOG_INFO(locals.log); return; } @@ -2892,7 +3214,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - output.errorCode = static_cast(EAuctionError::InsufficientFunds); + output.errorCode = EAuctionError::InsufficientFunds; + setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); + LOG_INFO(locals.log); return; } @@ -2900,7 +3224,7 @@ struct NOST : public ContractBase while (locals.participantIndex != NULL_INDEX) { locals.participantKey = state.get().participants.key(locals.participantIndex); - if (locals.participantKey.auctionId == input.auctionId) + if (locals.participantKey.auctionIndex == input.auctionIndex) { locals.participantData = state.get().participants.value(locals.participantIndex); if (locals.participantData.escrowedAmount > 0) @@ -2922,8 +3246,8 @@ struct NOST : public ContractBase locals.currentDate = qpi.now(); locals.auction.core.status = EAuctionStatus::Cancelled; locals.auction.core.settledAt = locals.currentDate; - state.mut().auctionList.replace(input.auctionId, locals.auction); - addClosedAuctionToHistory(state, locals.auction.core.auctionId); + state.mut().auctionList.replace(input.auctionIndex, locals.auction); + addClosedAuctionToHistory(state, locals.auction.core.auctionIndex); locals.distributeAuctionServiceFeeInput.feeAmount = output.cancellationFee; CALL(DistributeAuctionServiceFee, locals.distributeAuctionServiceFeeInput, locals.distributeAuctionServiceFeeOutput); @@ -2933,7 +3257,9 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - output.cancellationFee); } - output.errorCode = static_cast(EAuctionError::Success); + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); + LOG_INFO(locals.log); } /** @@ -2943,7 +3269,7 @@ struct NOST : public ContractBase PUBLIC_PROCEDURE_WITH_LOCALS(ResolvePendingStandardAuction) { output.refundedAmount = 0; - output.errorCode = static_cast(EAuctionError::InvalidInput); + output.errorCode = EAuctionError::InvalidInput; if (qpi.invocationReward() > 0) { @@ -2953,69 +3279,82 @@ struct NOST : public ContractBase CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); if (locals.isAuctionInteractionPausedOutput.isPaused) { - output.errorCode = static_cast(EAuctionError::AuctionPaused); + output.errorCode = EAuctionError::AuctionPaused; + setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); + LOG_INFO(locals.log); return; } if (input.acceptSale > 1) { + setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); + LOG_INFO(locals.log); return; } - if (!state.get().auctionList.get(input.auctionId, locals.auction)) + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { - output.errorCode = static_cast(EAuctionError::AuctionNotFound); + output.errorCode = EAuctionError::AuctionNotFound; + setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); + LOG_INFO(locals.log); return; } if (locals.auction.core.seller != qpi.invocator()) { - output.errorCode = static_cast(EAuctionError::Forbidden); + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); + LOG_INFO(locals.log); return; } if (locals.auction.core.type != EAuctionType::Standard || locals.auction.core.status != EAuctionStatus::PendingSellerDecision) { - output.errorCode = static_cast(EAuctionError::AuctionClosed); + output.errorCode = EAuctionError::AuctionClosed; + setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); + LOG_INFO(locals.log); return; } locals.currentDate = qpi.now(); if (!state.get().isAuctionTimerPaused && locals.auction.core.sellerDecisionDeadline <= locals.currentDate) { - locals.finalizeStandardAuctionInput.auctionId = input.auctionId; + locals.finalizeStandardAuctionInput.auctionIndex = input.auctionIndex; locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); - output.errorCode = static_cast(EAuctionError::AuctionClosed); + output.errorCode = EAuctionError::AuctionClosed; + setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); + LOG_INFO(locals.log); return; } if (input.acceptSale) { - locals.finalizeStandardAuctionInput.auctionId = input.auctionId; + locals.finalizeStandardAuctionInput.auctionIndex = input.auctionIndex; locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); - output.errorCode = locals.finalizeStandardAuctionOutput.success ? static_cast(EAuctionError::Success) - : static_cast(EAuctionError::AuctionClosed); + output.errorCode = locals.finalizeStandardAuctionOutput.success ? EAuctionError::Success : EAuctionError::AuctionClosed; } else { - locals.rejectStandardAuctionInput.auctionId = input.auctionId; + 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 ? static_cast(EAuctionError::Success) - : static_cast(EAuctionError::AuctionClosed); + output.errorCode = locals.rejectStandardAuctionOutput.success ? EAuctionError::Success : EAuctionError::AuctionClosed; } + setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); + LOG_INFO(locals.log); } /** * @brief Overwrites the full auction fee configuration. * @note Only the configured takeover coordinator can call this procedure. */ - PUBLIC_PROCEDURE(SetAuctionFees) + PUBLIC_PROCEDURE_WITH_LOCALS(SetAuctionFees) { + output.errorCode = EAuctionError::InvalidInput; if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); @@ -3023,7 +3362,9 @@ struct NOST : public ContractBase if (qpi.invocator() != state.get().takeoverCoordinator) { - output.errorCode = static_cast(EAuctionError::Forbidden); + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), 6, output.errorCode, 0, 0); + LOG_INFO(locals.log); return; } @@ -3032,7 +3373,9 @@ struct NOST : public ContractBase input.takeoverCoordinatorFeeBasisPoints, input.shareholderDividendBasisPoints, input.shareholderFeeBasisPointsTier1, input.shareholderFeeBasisPointsTier2, input.shareholderFeeBasisPointsTier3, input.shareholderFeeBasisPointsTier4)) { - output.errorCode = static_cast(EAuctionError::InvalidInput); + output.errorCode = EAuctionError::InvalidInput; + setProcedureLogInput(locals.log, qpi.invocator(), 6, output.errorCode, 0, 0); + LOG_INFO(locals.log); return; } @@ -3046,16 +3389,18 @@ struct NOST : public ContractBase state.mut().shareholderFeeBasisPointsTier2 = input.shareholderFeeBasisPointsTier2; state.mut().shareholderFeeBasisPointsTier3 = input.shareholderFeeBasisPointsTier3; state.mut().shareholderFeeBasisPointsTier4 = input.shareholderFeeBasisPointsTier4; - output.errorCode = static_cast(EAuctionError::Success); + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), 6, output.errorCode, 0, 0); + LOG_INFO(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(SetAuctionFeesByManagement) + PUBLIC_PROCEDURE_WITH_LOCALS(SetAuctionFeesByManagement) { - output.errorCode = static_cast(EAuctionError::InvalidInput); + output.errorCode = EAuctionError::InvalidInput; if (qpi.invocationReward() > 0) { @@ -3064,7 +3409,9 @@ struct NOST : public ContractBase if (qpi.invocator() != state.get().management) { - output.errorCode = static_cast(EAuctionError::Forbidden); + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), 7, output.errorCode, 0, 0); + LOG_INFO(locals.log); return; } @@ -3073,6 +3420,8 @@ struct NOST : public ContractBase state.get().takeoverCoordinatorFeeBasisPoints, state.get().shareholderDividendBasisPoints, input.shareholderFeeBasisPointsTier1, input.shareholderFeeBasisPointsTier2, input.shareholderFeeBasisPointsTier3, input.shareholderFeeBasisPointsTier4)) { + setProcedureLogInput(locals.log, qpi.invocator(), 7, output.errorCode, 0, 0); + LOG_INFO(locals.log); return; } @@ -3084,16 +3433,18 @@ struct NOST : public ContractBase state.mut().shareholderFeeBasisPointsTier2 = input.shareholderFeeBasisPointsTier2; state.mut().shareholderFeeBasisPointsTier3 = input.shareholderFeeBasisPointsTier3; state.mut().shareholderFeeBasisPointsTier4 = input.shareholderFeeBasisPointsTier4; - output.errorCode = static_cast(EAuctionError::Success); + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), 7, output.errorCode, 0, 0); + LOG_INFO(locals.log); } /** * @brief Reassigns the management role to another wallet. * @note Only the configured takeover coordinator can call this procedure. */ - PUBLIC_PROCEDURE(SetManagement) + PUBLIC_PROCEDURE_WITH_LOCALS(SetManagement) { - output.errorCode = static_cast(EAuctionError::InvalidInput); + output.errorCode = EAuctionError::InvalidInput; if (qpi.invocationReward() > 0) { @@ -3102,27 +3453,33 @@ struct NOST : public ContractBase if (qpi.invocator() != state.get().takeoverCoordinator) { - output.errorCode = static_cast(EAuctionError::Forbidden); + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), 8, output.errorCode, 0, 0); + LOG_INFO(locals.log); return; } if (isZero(input.management)) { + setProcedureLogInput(locals.log, qpi.invocator(), 8, output.errorCode, 0, 0); + LOG_INFO(locals.log); return; } state.mut().management = input.management; - output.errorCode = static_cast(EAuctionError::Success); + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), 8, output.errorCode, 0, 0); + LOG_INFO(locals.log); } /** * @brief Returns the stored state of one auction. * @note The response contains a serializable auction view; access-control sets are returned as fixed arrays with counts. */ - PUBLIC_FUNCTION_WITH_LOCALS(GetAuction) + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionByIndex) { output.found = 0; - if (!state.get().auctionList.get(input.auctionId, locals.auction)) + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { return; } @@ -3157,7 +3514,7 @@ struct NOST : public ContractBase */ PUBLIC_FUNCTION(GetAuctionParticipant) { - output.found = state.get().participants.get({input.auctionId, input.participant}, output.participantData) ? 1 : 0; + output.found = state.get().participants.get({input.auctionIndex, input.participant}, output.participantData) ? 1 : 0; } /** @@ -3214,7 +3571,7 @@ struct NOST : public ContractBase */ PUBLIC_FUNCTION(GetClosedAuctionHistory) { - output.auctionIds = state.get().closedAuctionHistory; + output.auctionIndices = state.get().closedAuctionHistory; output.totalEntries = state.get().closedAuctionHistoryCounter; } @@ -3223,6 +3580,232 @@ struct NOST : public ContractBase */ PUBLIC_FUNCTION(GetRouteAllFeesToDevelopment) { output.enabled = state.get().routeAllFeesToDevelopment; } + PUBLIC_FUNCTION_WITH_LOCALS(GetContractStats) + { + output.stats.totalAuctionsCreated = state.get().totalAuctionsCreated; + output.stats.participantCount = state.get().participants.population(); + output.stats.closedAuctionHistoryCounter = state.get().closedAuctionHistoryCounter; + output.stats.auctionShareholderDividendPool = state.get().auctionShareholderDividendPool; + output.stats.qxTransferFee = state.get().qxTransferFee; + output.stats.routeAllFeesToDevelopment = state.get().routeAllFeesToDevelopment; + output.stats.isAuctionTimerPaused = state.get().isAuctionTimerPaused; + output.stats.isPostBeginEpochPauseArmed = state.get().isPostBeginEpochPauseArmed; + + for (locals.auctionIndex = 0; locals.auctionIndex < state.get().totalAuctionsCreated; ++locals.auctionIndex) + { + if (!state.get().auctionList.get(locals.auctionIndex, locals.auction)) + { + continue; + } + 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; + case EAuctionStatus::Finalized: output.stats.finalizedAuctionCount = sadd(output.stats.finalizedAuctionCount, 1ULL); break; + case EAuctionStatus::Cancelled: output.stats.cancelledAuctionCount = sadd(output.stats.cancelledAuctionCount, 1ULL); break; + default: break; + } + } + } + + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionSummaries) + { + output.totalCount = state.get().totalAuctionsCreated; + output.returnedCount = 0; + locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + for (locals.auctionIndex = input.offset; locals.auctionIndex < state.get().totalAuctionsCreated && output.returnedCount < locals.boundedLimit; + ++locals.auctionIndex) + { + if (!state.get().auctionList.get(locals.auctionIndex, locals.auction)) + { + continue; + } + fillAuctionSummary(locals.auction, locals.auctionSummary); + output.auctions.set(output.returnedCount, locals.auctionSummary); + output.returnedCount = sadd(output.returnedCount, 1ULL); + } + } + + PUBLIC_FUNCTION_WITH_LOCALS(GetActiveAuctionIndices) + { + output.totalCount = 0; + output.returnedCount = 0; + locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + for (locals.auctionIndex = 0; locals.auctionIndex < state.get().totalAuctionsCreated; ++locals.auctionIndex) + { + if (!state.get().auctionList.get(locals.auctionIndex, locals.auction)) + { + continue; + } + if (locals.auction.core.status != EAuctionStatus::Active && locals.auction.core.status != EAuctionStatus::PendingSellerDecision) + { + continue; + } + if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) + { + output.auctionIndices.set(output.returnedCount, locals.auction.core.auctionIndex); + output.returnedCount = sadd(output.returnedCount, 1ULL); + } + output.totalCount = sadd(output.totalCount, 1ULL); + } + } + + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionsBySeller) + { + output.totalCount = 0; + output.returnedCount = 0; + locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + for (locals.auctionIndex = 0; locals.auctionIndex < state.get().totalAuctionsCreated; ++locals.auctionIndex) + { + if (!state.get().auctionList.get(locals.auctionIndex, locals.auction) || locals.auction.core.seller != input.seller) + { + continue; + } + if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) + { + fillAuctionSummary(locals.auction, locals.auctionSummary); + output.auctions.set(output.returnedCount, locals.auctionSummary); + output.returnedCount = sadd(output.returnedCount, 1ULL); + } + output.totalCount = sadd(output.totalCount, 1ULL); + } + } + + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionByMetadataCid) + { + output.found = 0; + output.auctionIndex = 0; + for (locals.auctionIndex = 0; locals.auctionIndex < state.get().totalAuctionsCreated; ++locals.auctionIndex) + { + if (!state.get().auctionList.get(locals.auctionIndex, locals.auction)) + { + continue; + } + locals.metadataMatches = 1; + for (locals.metadataIndex = 0; locals.metadataIndex < NOST_AUCTION_METADATA_CID_LENGTH; ++locals.metadataIndex) + { + if (locals.auction.core.metadataIpfsCid.get(locals.metadataIndex) != input.metadataIpfsCid.get(locals.metadataIndex)) + { + locals.metadataMatches = 0; + break; + } + } + if (locals.metadataMatches) + { + output.found = 1; + output.auctionIndex = locals.auction.core.auctionIndex; + fillAuctionSummary(locals.auction, output.auction); + return; + } + } + } + + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionSummariesByIndexBatch) + { + output.returnedCount = 0; + locals.boundedLimit = min(input.count, NOST_AUCTION_GETTER_PAGE_SIZE); + for (locals.requestedIndex = 0; locals.requestedIndex < locals.boundedLimit; ++locals.requestedIndex) + { + locals.auctionIndex = input.auctionIndices.get(locals.requestedIndex); + if (state.get().auctionList.get(locals.auctionIndex, locals.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); + } + } + } + + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionParticipants) + { + output.totalCount = 0; + output.returnedCount = 0; + locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + for (locals.participantMapIndex = state.get().participants.nextElementIndex(NULL_INDEX); locals.participantMapIndex != NULL_INDEX; + locals.participantMapIndex = state.get().participants.nextElementIndex(locals.participantMapIndex)) + { + locals.participantKey = state.get().participants.key(locals.participantMapIndex); + if (locals.participantKey.auctionIndex != input.auctionIndex) + { + continue; + } + if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) + { + locals.participantData = state.get().participants.value(locals.participantMapIndex); + fillParticipantSummary(locals.participantData, locals.participantSummary); + output.participants.set(output.returnedCount, locals.participantSummary); + output.returnedCount = sadd(output.returnedCount, 1ULL); + } + output.totalCount = sadd(output.totalCount, 1ULL); + } + } + + PUBLIC_FUNCTION_WITH_LOCALS(GetUserParticipations) + { + output.totalCount = 0; + output.returnedCount = 0; + locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + for (locals.participantMapIndex = state.get().participants.nextElementIndex(NULL_INDEX); locals.participantMapIndex != NULL_INDEX; + locals.participantMapIndex = state.get().participants.nextElementIndex(locals.participantMapIndex)) + { + locals.participantKey = state.get().participants.key(locals.participantMapIndex); + if (locals.participantKey.participant != input.participant) + { + continue; + } + if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) + { + locals.participantData = state.get().participants.value(locals.participantMapIndex); + fillUserParticipationSummary(locals.participantKey.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(GetLatestAuctionIndex) + { + output.found = state.get().totalAuctionsCreated > 0; + output.auctionIndex = output.found ? state.get().totalAuctionsCreated - 1 : 0; + } + + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionCountBySeller) + { + output.count = 0; + for (locals.auctionIndex = 0; locals.auctionIndex < state.get().totalAuctionsCreated; ++locals.auctionIndex) + { + if (state.get().auctionList.get(locals.auctionIndex, locals.auction) && locals.auction.core.seller == input.seller) + { + output.count = sadd(output.count, 1ULL); + } + } + } + + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionAtCreationSnapshot) + { + output.found = 0; + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + { + return; + } + 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); + } + /** * @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. @@ -3257,9 +3840,69 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), locals.refundAmount); } + setProcedureLogInput(locals.log, qpi.invocator(), 4, locals.success ? EAuctionError::Success : EAuctionError::InvalidInput, 0, + output.transferredNumberOfShares); + + LOG_INFO(locals.log); } protected: + static void setProcedureLogInput(NostromoProcedureLog& log, const id& actor, uint8 procedure, EAuctionError errorCode, uint64 auctionIndex, + sint64 amount) + { + log.contractIndex = SELF_INDEX; + log.procedure = procedure; + log.errorCode = errorCode; + log.auctionIndex = auctionIndex; + log.actor = actor; + log.amount = amount; + log._terminator = 0; + } + + static void fillAuctionSummary(const AuctionData& auction, AuctionSummary& summary) + { + 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); + } + + static void fillParticipantSummary(const AuctionParticipantData& participantData, ParticipantSummary& summary) + { + 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; + } + + static void fillUserParticipationSummary(uint64 auctionIndex, const AuctionParticipantData& participantData, UserParticipationSummary& summary) + { + 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; + } + template static constexpr T min(const T& a, const T& b) { @@ -3270,8 +3913,8 @@ struct NOST : public ContractBase { return a > b ? a : b; } - static bool resolveBatchAuctionCreateParams(uint64 lotItemCount, uint64 totalEscrowQuantity, - uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPrice) + static bool resolveBatchAuctionCreateParams(uint64 lotItemCount, uint64 totalEscrowQuantity, uint64& quantityForSale, + uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPrice) { quantityForSale = 0; resolvedMinimumPurchaseQuantity = 0; @@ -3415,9 +4058,9 @@ struct NOST : public ContractBase return state.get().routeAllFeesToDevelopment; } - static void addClosedAuctionToHistory(QPI::ContractState& state, const id& auctionId) + static void addClosedAuctionToHistory(QPI::ContractState& state, uint64 auctionIndex) { - state.mut().closedAuctionHistory.set(mod(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()), auctionId); + state.mut().closedAuctionHistory.set(mod(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()), auctionIndex); state.mut().closedAuctionHistoryCounter = sadd(state.get().closedAuctionHistoryCounter, 1ULL); } diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index e936c9bf1..de249e290 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -136,12 +136,12 @@ class ContractTestingNOST : protected ContractTesting return output; } - NOST::PlaceBid_output placeBid(const id& bidder, const id& auctionId, uint64 quantity, uint64 bidAmount, sint64 reward) + NOST::PlaceBid_output placeBid(const id& bidder, uint64 auctionIndex, uint64 quantity, uint64 bidAmount, sint64 reward) { NOST::PlaceBid_input input{}; NOST::PlaceBid_output output{}; - input.auctionId = auctionId; + input.auctionIndex = auctionIndex; input.quantity = quantity; input.bidAmount = bidAmount; @@ -150,12 +150,12 @@ class ContractTestingNOST : protected ContractTesting return output; } - NOST::PlaceBid_output placeBidWithFundedReward(const id& bidder, const id& auctionId, uint64 quantity, uint64 bidAmount, sint64 reward) + NOST::PlaceBid_output placeBidWithFundedReward(const id& bidder, uint64 auctionIndex, uint64 quantity, uint64 bidAmount, sint64 reward) { NOST::PlaceBid_input input{}; NOST::PlaceBid_output output{}; - input.auctionId = auctionId; + input.auctionIndex = auctionIndex; input.quantity = quantity; input.bidAmount = bidAmount; @@ -163,12 +163,12 @@ class ContractTestingNOST : protected ContractTesting return output; } - NOST::CancelAuction_output cancelAuction(const id& seller, const id& auctionId, sint64 reward) + NOST::CancelAuction_output cancelAuction(const id& seller, uint64 auctionIndex, sint64 reward) { NOST::CancelAuction_input input{}; NOST::CancelAuction_output output{}; - input.auctionId = auctionId; + input.auctionIndex = auctionIndex; if (reward > 0) { seedUser(seller, reward ); @@ -215,12 +215,12 @@ class ContractTestingNOST : protected ContractTesting return transferManagedSharesWithReward(owner, asset, numberOfShares, contractIndex, getCachedQxTransferFee()); } - NOST::ResolvePendingStandardAuction_output resolvePendingStandardAuction(const id& seller, const id& auctionId, bool acceptSale) + NOST::ResolvePendingStandardAuction_output resolvePendingStandardAuction(const id& seller, uint64 auctionIndex, bool acceptSale) { NOST::ResolvePendingStandardAuction_input input{}; NOST::ResolvePendingStandardAuction_output output{}; - input.auctionId = auctionId; + input.auctionIndex = auctionIndex; input.acceptSale = acceptSale ? 1 : 0; ensureUser(seller); @@ -255,22 +255,22 @@ class ContractTestingNOST : protected ContractTesting return output; } - NOST::GetAuction_output getAuction(const id& auctionId) const + NOST::GetAuctionByIndex_output getAuction(uint64 auctionIndex) const { - NOST::GetAuction_input input{}; - NOST::GetAuction_output output{}; + NOST::GetAuctionByIndex_input input{}; + NOST::GetAuctionByIndex_output output{}; - input.auctionId = auctionId; + input.auctionIndex = auctionIndex; callFunction(NOST_CONTRACT_INDEX, 1, input, output); return output; } - NOST::GetAuctionParticipant_output getParticipant(const id& auctionId, const id& participant) const + NOST::GetAuctionParticipant_output getParticipant(uint64 auctionIndex, const id& participant) const { NOST::GetAuctionParticipant_input input{}; NOST::GetAuctionParticipant_output output{}; - input.auctionId = auctionId; + input.auctionIndex = auctionIndex; input.participant = participant; callFunction(NOST_CONTRACT_INDEX, 2, input, output); return output; @@ -321,6 +321,124 @@ class ContractTestingNOST : protected ContractTesting 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::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]); } @@ -524,12 +642,12 @@ static bool containsAccessAsset(const Array& auctionIds, uint64 count, const id& auctionId) +static bool containsAuctionIndex(const Array& auctionIndices, uint64 count, uint64 auctionIndex) { - const uint64 boundedCount = count < auctionIds.capacity() ? count : auctionIds.capacity(); + const uint64 boundedCount = count < auctionIndices.capacity() ? count : auctionIndices.capacity(); for (uint64 index = 0; index < boundedCount; ++index) { - if (auctionIds.get(index) == auctionId) + if (auctionIndices.get(index) == auctionIndex) { return true; } @@ -558,13 +676,13 @@ TEST(ContractNostromoAuction, InitialStateAndGettersAuction) EXPECT_EQ(recipients.development, ContractTestingNOST::developmentWallet()); EXPECT_EQ(recipients.takeoverCoordinator, ContractTestingNOST::takeoverCoordinatorWallet()); - const id missingAuction(777, 0, 0, 0); + 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_TRUE(isZero(auctionOutput.auction.core.auctionId)); + EXPECT_EQ(auctionOutput.auction.core.auctionIndex, 0ULL); EXPECT_EQ(participantOutput.found, 0); EXPECT_EQ(launchPause.ticks, 0U); EXPECT_EQ(nostromo.getClosedAuctionHistory().totalEntries, 0ULL); @@ -582,6 +700,105 @@ TEST(ContractNostromoAuction, InitialStateAndGettersAuction) 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), 100, 150, 10); + 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.placeBid(bidderA, createA.auctionIndex, 2, 11, 22).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidderB, createA.auctionIndex, 1, 15, 15).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidderA, createC.auctionIndex, 1, 100, 100).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, TransferShareManagementRightsAuction) { ContractTestingNOST nostromo; @@ -687,7 +904,6 @@ TEST(ContractNostromoAuction, TransferShareManagementRightsRequiresInvocationRew EXPECT_EQ(nostromo.managedShares(asset, owner), 4); } } - TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) { ContractTestingNOST nostromo; @@ -700,11 +916,11 @@ TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 9, 25); const auto output = nostromo.createAuction(seller, input); - ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); - EXPECT_FALSE(isZero(output.auctionId)); + ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(output.auctionIndex, 0ULL); - const auto auction = nostromo.getAuction(output.auctionId).auction; - EXPECT_EQ(auction.core.auctionId, output.auctionId); + 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, 0ULL); EXPECT_EQ(auction.core.salePrice, 25ULL); @@ -737,9 +953,9 @@ TEST(ContractNostromoAuction, CreateStandardBundleAuctionEscrowsMixedLotAuction) auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeTwoAssetLot(assetA, 2, assetB, 3), 100, 150, 5); const auto output = nostromo.createAuction(seller, input); - ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); - const auto auction = nostromo.getAuction(output.auctionId).auction; + const auto auction = nostromo.getAuction(output.auctionIndex).auction; EXPECT_EQ(auction.core.quantityForSale, 1ULL); EXPECT_EQ(auction.core.minimumPurchaseQuantity, 1ULL); EXPECT_EQ(auction.core.initialPrice, 100ULL); @@ -768,14 +984,14 @@ TEST(ContractNostromoAuction, CreateStandardAuctionAcceptsMaximumLotEntriesAucti const auto createOutput = nostromo.createAuction( seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeFullLot(asset, 1), 100, 100, 1)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); EXPECT_EQ(nostromo.managedShares(asset, seller), 0); EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), static_cast(NOST_AUCTION_LOT_ITEM_NUM)); - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 100, 100).errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, 100, 100).errorCode, NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(nostromo.managedShares(asset, bidder), static_cast(NOST_AUCTION_LOT_ITEM_NUM)); EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 0); @@ -798,9 +1014,9 @@ TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); - const auto auction = nostromo.getAuction(output.auctionId).auction; + 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); @@ -825,9 +1041,9 @@ TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({gateAsset}); const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); - const auto auction = nostromo.getAuction(output.auctionId).auction; + 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); @@ -854,11 +1070,11 @@ TEST(ContractNostromoAuction, GetAuctionViewExposesAccessListsAndFoundFlagAuctio input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({walletA, walletB}); const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - const auto auctionOutput = nostromo.getAuction(createOutput.auctionId); + const auto auctionOutput = nostromo.getAuction(createOutput.auctionIndex); EXPECT_EQ(auctionOutput.found, 1); - EXPECT_EQ(auctionOutput.auction.core.auctionId, createOutput.auctionId); + 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)); @@ -883,9 +1099,9 @@ TEST(ContractNostromoAuction, GetAuctionViewExposesAccessListsAndFoundFlagAuctio input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({accessAssetA, accessAssetB}); const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - const auto auctionOutput = nostromo.getAuction(createOutput.auctionId); + 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, accessAssetA)); @@ -895,10 +1111,10 @@ TEST(ContractNostromoAuction, GetAuctionViewExposesAccessListsAndFoundFlagAuctio { ContractTestingNOST nostromo; - const id missingAuction(999, 998, 997, 996); + const uint64 missingAuction = 999; const auto auctionOutput = nostromo.getAuction(missingAuction); EXPECT_EQ(auctionOutput.found, 0); - EXPECT_TRUE(isZero(auctionOutput.auction.core.auctionId)); + EXPECT_EQ(auctionOutput.auction.core.auctionIndex, 0ULL); } } @@ -919,9 +1135,9 @@ TEST(ContractNostromoAuction, GetAuctionViewDeduplicatesPrivateAccessInputsAucti input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({wallet, wallet}); const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; EXPECT_EQ(auction.allowedBidderWalletCount, 1U); EXPECT_TRUE(containsWallet(auction.allowedBidderWallets, auction.allowedBidderWalletCount, wallet)); } @@ -942,9 +1158,9 @@ TEST(ContractNostromoAuction, GetAuctionViewDeduplicatesPrivateAccessInputsAucti input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({accessAsset, accessAsset}); const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; EXPECT_EQ(auction.requiredAccessAssetCount, 1U); EXPECT_TRUE(containsAccessAsset(auction.requiredAccessAssets, auction.requiredAccessAssetCount, accessAsset)); } @@ -972,13 +1188,13 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacit 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, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - const auto auctionOutput = nostromo.getAuction(createOutput.auctionId); + 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.placeBid(allowedBidder, createOutput.auctionId, 1, 10, 10).errorCode, - static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(nostromo.placeBid(allowedBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, + NOST::EAuctionError::Success); } { @@ -1005,14 +1221,14 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacit 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, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - const auto auctionOutput = nostromo.getAuction(createOutput.auctionId); + 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, Asset{accessBidder, bidderAccessAssetName})); - EXPECT_EQ(nostromo.placeBid(accessBidder, createOutput.auctionId, 1, 10, 10).errorCode, - static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(nostromo.placeBid(accessBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, + NOST::EAuctionError::Success); } } @@ -1048,7 +1264,7 @@ TEST(ContractNostromoAuction, PrivateAuctionFeeIsDistributedAcrossRecipientsAuct input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); const auto output = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(output.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); EXPECT_EQ(getBalance(seller) - sellerBefore, 0); if (routeMode != 0) @@ -1087,73 +1303,73 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) auto invalidCid = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); invalidCid.metadataIpfsCid = ContractTestingNOST::makeInvalidMetadataCidFirstChar(); - EXPECT_EQ(nostromo.createAuction(seller, invalidCid).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + EXPECT_EQ(nostromo.createAuction(seller, invalidCid).errorCode, NOST::EAuctionError::InvalidInput); auto invalidCidUppercase = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); invalidCidUppercase.metadataIpfsCid = ContractTestingNOST::makeInvalidMetadataCidUppercase(); - EXPECT_EQ(nostromo.createAuction(seller, invalidCidUppercase).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + EXPECT_EQ(nostromo.createAuction(seller, invalidCidUppercase).errorCode, NOST::EAuctionError::InvalidInput); auto emptyLot = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); emptyLot.auctionLotItems = Array{}; - EXPECT_EQ(nostromo.createAuction(seller, emptyLot).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + EXPECT_EQ(nostromo.createAuction(seller, emptyLot).errorCode, NOST::EAuctionError::InvalidInput); auto negativeQuantity = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); negativeQuantity.auctionLotItems = ContractTestingNOST::makeSingleLot(assetA, -1); - EXPECT_EQ(nostromo.createAuction(seller, negativeQuantity).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + EXPECT_EQ(nostromo.createAuction(seller, negativeQuantity).errorCode, NOST::EAuctionError::InvalidInput); auto zeroDuration = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); zeroDuration.durationDays = 0; - EXPECT_EQ(nostromo.createAuction(seller, zeroDuration).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + EXPECT_EQ(nostromo.createAuction(seller, zeroDuration).errorCode, NOST::EAuctionError::InvalidInput); auto tooLongDuration = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); tooLongDuration.durationDays = NOST_AUCTION_MAX_DURATION_DAYS + 1; - EXPECT_EQ(nostromo.createAuction(seller, tooLongDuration).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + EXPECT_EQ(nostromo.createAuction(seller, tooLongDuration).errorCode, NOST::EAuctionError::InvalidInput); auto invalidType = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); invalidType.auctionType = 99; - EXPECT_EQ(nostromo.createAuction(seller, invalidType).errorCode, static_cast(NOST::EAuctionError::InvalidAuctionType)); + EXPECT_EQ(nostromo.createAuction(seller, invalidType).errorCode, NOST::EAuctionError::InvalidAuctionType); auto invalidVisibility = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); invalidVisibility.auctionVisibility = 99; - EXPECT_EQ(nostromo.createAuction(seller, invalidVisibility).errorCode, static_cast(NOST::EAuctionError::InvalidVisibility)); + EXPECT_EQ(nostromo.createAuction(seller, invalidVisibility).errorCode, NOST::EAuctionError::InvalidVisibility); auto invalidBatchBundle = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); invalidBatchBundle.auctionLotItems = ContractTestingNOST::makeTwoAssetLot(assetA, 2, assetB, 3); - EXPECT_EQ(nostromo.createAuction(seller, invalidBatchBundle).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + EXPECT_EQ(nostromo.createAuction(seller, invalidBatchBundle).errorCode, NOST::EAuctionError::InvalidInput); auto invalidBatchBuyNow = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); invalidBatchBuyNow.buyNowPrice = 100; - EXPECT_EQ(nostromo.createAuction(seller, invalidBatchBuyNow).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + EXPECT_EQ(nostromo.createAuction(seller, invalidBatchBuyNow).errorCode, NOST::EAuctionError::InvalidInput); auto invalidStandardMinimumPurchase = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); invalidStandardMinimumPurchase.minimumPurchaseQuantity = 2; - EXPECT_EQ(nostromo.createAuction(seller, invalidStandardMinimumPurchase).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + EXPECT_EQ(nostromo.createAuction(seller, invalidStandardMinimumPurchase).errorCode, NOST::EAuctionError::InvalidInput); auto invalidStandardIncrement = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); invalidStandardIncrement.minimumBidIncrement = 0; - EXPECT_EQ(nostromo.createAuction(seller, invalidStandardIncrement).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + EXPECT_EQ(nostromo.createAuction(seller, invalidStandardIncrement).errorCode, NOST::EAuctionError::InvalidInput); auto invalidStandardPrice = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1), 200, 150, 10); - EXPECT_EQ(nostromo.createAuction(seller, invalidStandardPrice).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + EXPECT_EQ(nostromo.createAuction(seller, invalidStandardPrice).errorCode, NOST::EAuctionError::InvalidInput); auto invalidStandardSalePrice = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); invalidStandardSalePrice.salePrice = 0; - EXPECT_EQ(nostromo.createAuction(seller, invalidStandardSalePrice).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + EXPECT_EQ(nostromo.createAuction(seller, invalidStandardSalePrice).errorCode, NOST::EAuctionError::InvalidInput); auto invalidStandardBuyNow = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1), 100, 150, 10, 140); - EXPECT_EQ(nostromo.createAuction(seller, invalidStandardBuyNow).errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + EXPECT_EQ(nostromo.createAuction(seller, invalidStandardBuyNow).errorCode, NOST::EAuctionError::InvalidInput); auto privateWithoutGate = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); privateWithoutGate.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); EXPECT_EQ(nostromo.createAuction(seller, privateWithoutGate, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, - static_cast(NOST::EAuctionError::InvalidInput)); + NOST::EAuctionError::InvalidInput); auto privateWithBothGates = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); privateWithBothGates.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); privateWithBothGates.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({id(99, 1, 1, 1)}); privateWithBothGates.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({Asset{altIssuer, assetNameFromString("GATINV")}}); EXPECT_EQ(nostromo.createAuction(seller, privateWithBothGates, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, - static_cast(NOST::EAuctionError::InvalidInput)); + NOST::EAuctionError::InvalidInput); } TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientAssetBalanceAndPauseAuction) @@ -1172,7 +1388,7 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientA 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, static_cast(NOST::EAuctionError::InsufficientFunds)); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::InsufficientFunds); EXPECT_EQ(nostromo.managedShares(asset, seller), 4); } @@ -1187,7 +1403,7 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientA auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 3, 10); const auto output = nostromo.createAuction(seller, input); - EXPECT_EQ(output.errorCode, static_cast(NOST::EAuctionError::InsufficientAssetBalance)); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::InsufficientAssetBalance); EXPECT_EQ(nostromo.managedShares(asset, seller), 2); } @@ -1205,8 +1421,8 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientA nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); const auto output = nostromo.createAuction(seller, input); - EXPECT_EQ(output.errorCode, static_cast(NOST::EAuctionError::AuctionPaused)); - EXPECT_TRUE(isZero(output.auctionId)); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::AuctionPaused); + EXPECT_EQ(output.auctionIndex, 0ULL); EXPECT_EQ(nostromo.managedShares(asset, seller), 3); } @@ -1224,8 +1440,8 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientA nostromo.advanceTicks(NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS); const auto output = nostromo.createAuction(seller, input); - EXPECT_EQ(output.errorCode, static_cast(NOST::EAuctionError::AuctionPaused)); - EXPECT_TRUE(isZero(output.auctionId)); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::AuctionPaused); + EXPECT_EQ(output.auctionIndex, 0ULL); EXPECT_EQ(nostromo.managedShares(asset, seller), 3); } } @@ -1240,10 +1456,10 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsWhenAuctionStorageIsFullAuctio for (uint64 index = 0; index < NOST_AUCTION_NUM; ++index) { NOST::AuctionData auction{}; - auction.core.auctionId = id(10000 + index, 11000 + index, 12000 + index, 13000 + index); + auction.core.auctionIndex = index; auction.core.seller = seller; auction.core.status = NOST::EAuctionStatus::Active; - ASSERT_NE(nostromo.stateData().auctionList.set(auction.core.auctionId, auction), NULL_INDEX); + ASSERT_NE(nostromo.stateData().auctionList.set(auction.core.auctionIndex, auction), NULL_INDEX); } ASSERT_EQ(nostromo.stateData().auctionList.population(), NOST_AUCTION_NUM); @@ -1251,8 +1467,8 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsWhenAuctionStorageIsFullAuctio EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); const auto output = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10)); - EXPECT_EQ(output.errorCode, static_cast(NOST::EAuctionError::StorageFull)); - EXPECT_TRUE(isZero(output.auctionId)); + EXPECT_EQ(output.errorCode, NOST::EAuctionError::StorageFull); + EXPECT_EQ(output.auctionIndex, 0ULL); EXPECT_EQ(nostromo.managedShares(asset, seller), 1); } @@ -1268,7 +1484,7 @@ TEST(ContractNostromoAuction, PlaceBidRejectsWhenParticipantStorageIsFullAuction EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 1, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); for (uint64 index = 0; index < NOST_AUCTION_PARTICIPANT_NUM; ++index) { @@ -1276,15 +1492,15 @@ TEST(ContractNostromoAuction, PlaceBidRejectsWhenParticipantStorageIsFullAuction participant.participant = id(14000 + index, 15000 + index, 16000 + index, 17000 + index); participant.bidAmount = 1; participant.requestedQuantity = 1; - NOST::AuctionParticipantKey key{id(18000 + index, 19000 + index, 20000 + index, 21000 + index), participant.participant}; + NOST::AuctionParticipantKey key{index + 100000ULL, participant.participant}; ASSERT_NE(nostromo.stateData().participants.set(key, participant), NULL_INDEX); } ASSERT_EQ(nostromo.stateData().participants.population(), NOST_AUCTION_PARTICIPANT_NUM); - const auto output = nostromo.placeBid(bidder, createOutput.auctionId, 1, 10, 10); - EXPECT_EQ(output.errorCode, static_cast(NOST::EAuctionError::StorageFull)); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.highestBidAmount, 0ULL); - EXPECT_EQ(nostromo.getParticipant(createOutput.auctionId, bidder).found, 0); + const auto output = nostromo.placeBid(bidder, createOutput.auctionIndex, 1, 10, 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) @@ -1301,54 +1517,54 @@ TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAucti EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 6), 6); const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 6, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - const auto sellerBid = nostromo.placeBid(seller, createOutput.auctionId, 1, 12, 12); - EXPECT_EQ(sellerBid.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + const auto sellerBid = nostromo.placeBid(seller, createOutput.auctionIndex, 1, 12, 12); + EXPECT_EQ(sellerBid.errorCode, NOST::EAuctionError::Forbidden); - const auto missingAuction = nostromo.placeBid(bidderA, id(700, 0, 0, 0), 1, 12, 12); - EXPECT_EQ(missingAuction.errorCode, static_cast(NOST::EAuctionError::AuctionNotFound)); + const auto missingAuction = nostromo.placeBid(bidderA, 700, 1, 12, 12); + EXPECT_EQ(missingAuction.errorCode, NOST::EAuctionError::AuctionNotFound); - const auto zeroQuantity = nostromo.placeBid(bidderA, createOutput.auctionId, 0, 12, 12); - EXPECT_EQ(zeroQuantity.errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + 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.auctionId, 1, 0, 1); - EXPECT_EQ(zeroBid.errorCode, static_cast(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.auctionId, 1, 9, 9); - EXPECT_EQ(tooLow.errorCode, static_cast(NOST::EAuctionError::BidTooLow)); + 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.auctionId, 2, 12, 23); - EXPECT_EQ(insufficientFunds.errorCode, static_cast(NOST::EAuctionError::InsufficientFunds)); + const auto insufficientFunds = nostromo.placeBid(bidderA, createOutput.auctionIndex, 2, 12, 23); + EXPECT_EQ(insufficientFunds.errorCode, NOST::EAuctionError::InsufficientFunds); - const auto bidA1 = nostromo.placeBid(bidderA, createOutput.auctionId, 2, 20, 40); - const auto bidB = nostromo.placeBid(bidderB, createOutput.auctionId, 3, 15, 45); - ASSERT_EQ(bidA1.errorCode, static_cast(NOST::EAuctionError::Success)); - ASSERT_EQ(bidB.errorCode, static_cast(NOST::EAuctionError::Success)); + const auto bidA1 = nostromo.placeBid(bidderA, createOutput.auctionIndex, 2, 20, 40); + const auto bidB = nostromo.placeBid(bidderB, createOutput.auctionIndex, 3, 15, 45); + ASSERT_EQ(bidA1.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(bidB.errorCode, NOST::EAuctionError::Success); - auto auction = nostromo.getAuction(createOutput.auctionId).auction; + 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.auctionId, 2, 14, 28); - EXPECT_EQ(bidA2.errorCode, static_cast(NOST::EAuctionError::Success)); + const auto bidA2 = nostromo.placeBid(bidderA, createOutput.auctionIndex, 2, 14, 28); + EXPECT_EQ(bidA2.errorCode, NOST::EAuctionError::Success); EXPECT_EQ(bidA2.escrowedAmount, 28ULL); EXPECT_EQ(bidA2.refundedAmount, 40ULL); - auction = nostromo.getAuction(createOutput.auctionId).auction; + auction = nostromo.getAuction(createOutput.auctionIndex).auction; EXPECT_EQ(auction.core.highestBidder, bidderB); EXPECT_EQ(auction.core.highestBidPrice, 15ULL); EXPECT_EQ(auction.core.highestBidAmount, 45ULL); - const auto participantA = nostromo.getParticipant(createOutput.auctionId, bidderA); + const auto participantA = nostromo.getParticipant(createOutput.auctionIndex, bidderA); ASSERT_EQ(participantA.found, 1); EXPECT_EQ(participantA.participantData.escrowedAmount, 28ULL); EXPECT_EQ(participantA.participantData.bidAmount, 14ULL); nostromo.setNow(2026, 1, 2, 9, 0, 1); - const auto closed = nostromo.placeBid(bidderC, createOutput.auctionId, 1, 30, 30); - EXPECT_EQ(closed.errorCode, static_cast(NOST::EAuctionError::AuctionClosed)); + const auto closed = nostromo.placeBid(bidderC, createOutput.auctionIndex, 1, 30, 30); + EXPECT_EQ(closed.errorCode, NOST::EAuctionError::AuctionClosed); } TEST(ContractNostromoAuction, PlaceBatchBidExtendsAuctionNearEndAuction) @@ -1363,13 +1579,13 @@ TEST(ContractNostromoAuction, PlaceBatchBidExtendsAuctionNearEndAuction) EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); nostromo.setNow(2026, 1, 2, 8, 56, 30); - const auto bidOutput = nostromo.placeBid(bidder, createOutput.auctionId, 1, 15, 15); - ASSERT_EQ(bidOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + const auto bidOutput = nostromo.placeBid(bidder, createOutput.auctionIndex, 1, 15, 15); + ASSERT_EQ(bidOutput.errorCode, NOST::EAuctionError::Success); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; EXPECT_EQ(auction.core.auctionDurationSeconds, NOST_SECONDS_PER_DAY + NOST_AUCTION_EXTENSION_SECONDS); } @@ -1387,27 +1603,27 @@ TEST(ContractNostromoAuction, PlaceStandardBidValidatesRefundsAndPauseAuction) const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - const auto sellerBid = nostromo.placeBid(seller, createOutput.auctionId, 1, 100, 100); - EXPECT_EQ(sellerBid.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + const auto sellerBid = nostromo.placeBid(seller, createOutput.auctionIndex, 1, 100, 100); + EXPECT_EQ(sellerBid.errorCode, NOST::EAuctionError::Forbidden); - const auto lowStart = nostromo.placeBid(bidderA, createOutput.auctionId, 1, 99, 99); - EXPECT_EQ(lowStart.errorCode, static_cast(NOST::EAuctionError::BidTooLow)); + const auto lowStart = nostromo.placeBid(bidderA, createOutput.auctionIndex, 1, 99, 99); + EXPECT_EQ(lowStart.errorCode, NOST::EAuctionError::BidTooLow); - const auto openingBid = nostromo.placeBid(bidderA, createOutput.auctionId, 1, 100, 100); - ASSERT_EQ(openingBid.errorCode, static_cast(NOST::EAuctionError::Success)); + const auto openingBid = nostromo.placeBid(bidderA, createOutput.auctionIndex, 1, 100, 100); + ASSERT_EQ(openingBid.errorCode, NOST::EAuctionError::Success); EXPECT_EQ(openingBid.escrowedAmount, 100ULL); - const auto lowIncrement = nostromo.placeBid(bidderB, createOutput.auctionId, 1, 109, 109); - EXPECT_EQ(lowIncrement.errorCode, static_cast(NOST::EAuctionError::BidTooLow)); + const auto lowIncrement = nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, 109, 109); + EXPECT_EQ(lowIncrement.errorCode, NOST::EAuctionError::BidTooLow); - const auto outbid = nostromo.placeBid(bidderB, createOutput.auctionId, 1, 110, 110); - ASSERT_EQ(outbid.errorCode, static_cast(NOST::EAuctionError::Success)); + const auto outbid = nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, 110, 110); + ASSERT_EQ(outbid.errorCode, NOST::EAuctionError::Success); EXPECT_EQ(outbid.refundedAmount, 100ULL); - const auto bidderAState = nostromo.getParticipant(createOutput.auctionId, bidderA); - const auto bidderBState = nostromo.getParticipant(createOutput.auctionId, bidderB); + 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); @@ -1415,25 +1631,25 @@ TEST(ContractNostromoAuction, PlaceStandardBidValidatesRefundsAndPauseAuction) EXPECT_EQ(bidderBState.participantData.escrowedAmount, 110ULL); EXPECT_EQ(bidderBState.participantData.isWinningBid, 1u); - const auto bidderBImprove = nostromo.placeBid(bidderB, createOutput.auctionId, 1, 130, 130); - EXPECT_EQ(bidderBImprove.errorCode, static_cast(NOST::EAuctionError::Success)); + const auto bidderBImprove = nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, 130, 130); + EXPECT_EQ(bidderBImprove.errorCode, NOST::EAuctionError::Success); EXPECT_EQ(bidderBImprove.refundedAmount, 110ULL); EXPECT_EQ(bidderBImprove.escrowedAmount, 130ULL); 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.auctionId, 1, 140, 140); - EXPECT_EQ(pausedBid.errorCode, static_cast(NOST::EAuctionError::AuctionPaused)); + const auto pausedBid = nostromo.placeBid(id(133, 134, 135, 136), createOutput.auctionIndex, 1, 140, 140); + 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.auctionId, 1, 140, 140); - EXPECT_EQ(resumedBid.errorCode, static_cast(NOST::EAuctionError::Success)); + const auto resumedBid = nostromo.placeBid(id(137, 138, 139, 140), createOutput.auctionIndex, 1, 140, 140); + 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.auctionId, 1, 150, 150); - EXPECT_EQ(bootstrapPausedBid.errorCode, static_cast(NOST::EAuctionError::AuctionPaused)); + const auto bootstrapPausedBid = nostromo.placeBid(id(141, 142, 143, 144), createOutput.auctionIndex, 1, 150, 150); + EXPECT_EQ(bootstrapPausedBid.errorCode, NOST::EAuctionError::AuctionPaused); } TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) @@ -1454,11 +1670,11 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowed}); const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.placeBid(denied, createOutput.auctionId, 1, 12, 12).errorCode, - static_cast(NOST::EAuctionError::PrivateAuctionAccessDenied)); - EXPECT_EQ(nostromo.placeBid(allowed, createOutput.auctionId, 1, 12, 12).errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(nostromo.placeBid(denied, createOutput.auctionIndex, 1, 12, 12).errorCode, + NOST::EAuctionError::PrivateAuctionAccessDenied); + EXPECT_EQ(nostromo.placeBid(allowed, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::Success); } { @@ -1480,11 +1696,11 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({accessAsset}); const auto createOutput = nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.placeBid(denied, createOutput.auctionId, 1, 12, 12).errorCode, - static_cast(NOST::EAuctionError::PrivateAuctionAccessDenied)); - EXPECT_EQ(nostromo.placeBid(allowed, createOutput.auctionId, 1, 12, 12).errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(nostromo.placeBid(denied, createOutput.auctionIndex, 1, 12, 12).errorCode, + NOST::EAuctionError::PrivateAuctionAccessDenied); + EXPECT_EQ(nostromo.placeBid(allowed, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::Success); } } @@ -1507,14 +1723,14 @@ TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) NOST::AuctionRevenueBreakdown expectedRevenue{}; nostromo.calculateAuctionRevenueBreakdown(180ULL, expectedRevenue); const auto createOutput = nostromo.createAuction(seller, input); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); const sint64 sellerBalanceBefore = getBalance(seller); - const auto bidOutput = nostromo.placeBid(bidder, createOutput.auctionId, 1, 180, 180); - ASSERT_EQ(bidOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + const auto bidOutput = nostromo.placeBid(bidder, createOutput.auctionIndex, 1, 180, 180); + ASSERT_EQ(bidOutput.errorCode, NOST::EAuctionError::Success); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; - const auto participant = nostromo.getParticipant(createOutput.auctionId, bidder); + 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); @@ -1541,20 +1757,20 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 4), 4); const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 4, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(bidderA, createOutput.auctionId, 3, 15, 45).errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidderA, createOutput.auctionIndex, 3, 15, 45).errorCode, NOST::EAuctionError::Success); nostromo.setNow(2026, 1, 1, 9, 0, 1); - ASSERT_EQ(nostromo.placeBid(bidderB, createOutput.auctionId, 3, 15, 45).errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 3, 15, 45).errorCode, NOST::EAuctionError::Success); nostromo.setNow(2026, 1, 1, 9, 0, 2); - ASSERT_EQ(nostromo.placeBid(bidderC, createOutput.auctionId, 2, 20, 40).errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidderC, createOutput.auctionIndex, 2, 20, 40).errorCode, NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; - const auto participantA = nostromo.getParticipant(createOutput.auctionId, bidderA); - const auto participantB = nostromo.getParticipant(createOutput.auctionId, bidderB); - const auto participantC = nostromo.getParticipant(createOutput.auctionId, bidderC); + 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); @@ -1587,12 +1803,12 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 5), 5); const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 5, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 2, 12, 24).errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 2, 12, 24).errorCode, NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + 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); @@ -1614,7 +1830,7 @@ TEST(ContractNostromoAuction, BatchFinalizationRefundsLosingBidsAndTieBreaksByBi EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 1, 5)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); nostromo.seedUser(earlierBidder, 100); nostromo.seedUser(laterBidder, 100); @@ -1623,20 +1839,20 @@ TEST(ContractNostromoAuction, BatchFinalizationRefundsLosingBidsAndTieBreaksByBi const sint64 laterBefore = getBalance(laterBidder); const sint64 lowerBefore = getBalance(lowerBidder); - ASSERT_EQ(nostromo.placeBidWithFundedReward(earlierBidder, createOutput.auctionId, 1, 10, 10).errorCode, - static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBidWithFundedReward(earlierBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, + NOST::EAuctionError::Success); nostromo.setNow(2026, 1, 1, 9, 0, 1); - ASSERT_EQ(nostromo.placeBidWithFundedReward(laterBidder, createOutput.auctionId, 1, 10, 10).errorCode, - static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBidWithFundedReward(laterBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, + NOST::EAuctionError::Success); nostromo.setNow(2026, 1, 1, 9, 0, 2); - ASSERT_EQ(nostromo.placeBidWithFundedReward(lowerBidder, createOutput.auctionId, 1, 9, 9).errorCode, - static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBidWithFundedReward(lowerBidder, createOutput.auctionIndex, 1, 9, 9).errorCode, + NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto earlier = nostromo.getParticipant(createOutput.auctionId, earlierBidder); - const auto later = nostromo.getParticipant(createOutput.auctionId, laterBidder); - const auto lower = nostromo.getParticipant(createOutput.auctionId, lowerBidder); + const auto earlier = nostromo.getParticipant(createOutput.auctionIndex, earlierBidder); + const auto later = nostromo.getParticipant(createOutput.auctionIndex, laterBidder); + const auto lower = nostromo.getParticipant(createOutput.auctionIndex, lowerBidder); ASSERT_EQ(earlier.found, 1); ASSERT_EQ(later.found, 1); ASSERT_EQ(lower.found, 1); @@ -1666,11 +1882,11 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + 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)); @@ -1690,19 +1906,19 @@ TEST(ContractNostromoAuction, WeeklyPauseShiftsActiveAuctionDeadlineAuction) const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); nostromo.setNow(2026, 1, 7, 11, 40, 0); nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.status, NOST::EAuctionStatus::Active); + 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.auctionId).auction.core.status, NOST::EAuctionStatus::Active); + 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.auctionId).auction.core.status, NOST::EAuctionStatus::Finalized); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(nostromo.managedShares(asset, seller), 1); } @@ -1719,12 +1935,12 @@ TEST(ContractNostromoAuction, EndTickSkipsAuctionProcessingAtBootstrapTimeAuctio const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); nostromo.setNow(2022, 4, 13, 12, 0, 0); nostromo.advanceAndEndTick(0); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + 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); @@ -1743,11 +1959,11 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, 120, 120).errorCode, NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY) * 1000ULL); - auto auction = nostromo.getAuction(createOutput.auctionId).auction; + 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); @@ -1764,12 +1980,12 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) nostromo.setNow(2026, 1, 9, 9, 8, 10); nostromo.advanceAndEndTick(0); - auction = nostromo.getAuction(createOutput.auctionId).auction; + 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.auctionId).auction; + 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); @@ -1779,14 +1995,14 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) nostromo.setNow(shiftedDeadline.getYear(), shiftedDeadline.getMonth(), shiftedDeadline.getDay(), shiftedDeadline.getHour(), shiftedDeadline.getMinute(), shiftedDeadline.getSecond()); nostromo.advanceAndEndTick(0); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); + 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.auctionId).auction; + auction = nostromo.getAuction(createOutput.auctionIndex).auction; EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); } @@ -1813,17 +2029,17 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) const auto createOutput = nostromo.createAuction( seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 10000, 10000, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); 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.auctionId, 1, 10000, 10000).errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, 10000, 10000).errorCode, NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; + 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); @@ -1860,18 +2076,18 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, 120, 120).errorCode, NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); - const auto forbidden = nostromo.resolvePendingStandardAuction(id(999, 999, 999, 999), createOutput.auctionId, true); - EXPECT_EQ(forbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + 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.auctionId, true); - EXPECT_EQ(acceptOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.status, NOST::EAuctionStatus::Finalized); + 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); } @@ -1887,19 +2103,19 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); nostromo.seedUser(bidder, 500); const sint64 bidderBeforeBid = getBalance(bidder); - ASSERT_EQ(nostromo.placeBidWithFundedReward(bidder, createOutput.auctionId, 1, 120, 120).errorCode, - static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(nostromo.placeBidWithFundedReward(bidder, createOutput.auctionIndex, 1, 120, 120).errorCode, + NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - const auto rejectOutput = nostromo.resolvePendingStandardAuction(seller, createOutput.auctionId, false); - EXPECT_EQ(rejectOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + const auto rejectOutput = nostromo.resolvePendingStandardAuction(seller, createOutput.auctionIndex, false); + EXPECT_EQ(rejectOutput.errorCode, NOST::EAuctionError::Success); EXPECT_EQ(rejectOutput.refundedAmount, 120ULL); - const auto auction = nostromo.getAuction(createOutput.auctionId).auction; - const auto participant = nostromo.getParticipant(createOutput.auctionId, bidder); + 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)); @@ -1922,15 +2138,15 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 100, 150, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 120, 120).errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, 120, 120).errorCode, NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.status, NOST::EAuctionStatus::PendingSellerDecision); + 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.auctionId).auction; - const auto participant = nostromo.getParticipant(createOutput.auctionId, bidder); + 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); @@ -1956,7 +2172,7 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 10), 10); const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 1000)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); const sint64 sellerBefore = getBalance(seller); const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); @@ -1966,11 +2182,11 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) nostromo.calculateAuctionServiceFeeBreakdown(1000ULL, expectedBreakdown); const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedBreakdown.shareholderDividendAmount); - const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionId, 1000); - EXPECT_EQ(cancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + 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.auctionId).auction.core.status, NOST::EAuctionStatus::Cancelled); + 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); if (routeMode != 0) @@ -2003,7 +2219,7 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) const auto createOutput = nostromo.createAuction( seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 8000, 10000, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); const sint64 sellerBefore = getBalance(seller); const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); @@ -2013,11 +2229,11 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) nostromo.calculateAuctionServiceFeeBreakdown(1000ULL, expectedBreakdown); const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedBreakdown.shareholderDividendAmount); - const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionId, 1000); - EXPECT_EQ(cancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + 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.auctionId).auction.core.status, NOST::EAuctionStatus::Cancelled); + 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); if (routeMode != 0) @@ -2056,7 +2272,7 @@ TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeR EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(batchSeller, batchAsset, 7), 7); const auto batchCreateOutput = nostromo.createAuction(batchSeller, ContractTestingNOST::makeBatchAuctionInput(batchAsset, 7, 333)); - ASSERT_EQ(batchCreateOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(batchCreateOutput.errorCode, NOST::EAuctionError::Success); NOST::AuctionServiceFeeBreakdown expectedBatchBreakdown{}; nostromo.calculateAuctionServiceFeeBreakdown(233ULL, expectedBatchBreakdown); @@ -2066,8 +2282,8 @@ TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeR const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - const auto batchCancelOutput = nostromo.cancelAuction(batchSeller, batchCreateOutput.auctionId, 233); - EXPECT_EQ(batchCancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + const auto batchCancelOutput = nostromo.cancelAuction(batchSeller, batchCreateOutput.auctionIndex, 233); + EXPECT_EQ(batchCancelOutput.errorCode, NOST::EAuctionError::Success); EXPECT_EQ(batchCancelOutput.cancellationFee, 233ULL); if (routeMode != 0) { @@ -2102,7 +2318,7 @@ TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeR const auto standardCreateOutput = smallFeeNostromo.createAuction( standardSeller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(standardAsset, 1), 19, 19, 1)); - ASSERT_EQ(standardCreateOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(standardCreateOutput.errorCode, NOST::EAuctionError::Success); NOST::AuctionServiceFeeBreakdown expectedSmallBreakdown{}; smallFeeNostromo.calculateAuctionServiceFeeBreakdown(1ULL, expectedSmallBreakdown); @@ -2111,8 +2327,8 @@ TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeR const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - const auto standardCancelOutput = smallFeeNostromo.cancelAuction(standardSeller, standardCreateOutput.auctionId, 1); - EXPECT_EQ(standardCancelOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + const auto standardCancelOutput = smallFeeNostromo.cancelAuction(standardSeller, standardCreateOutput.auctionIndex, 1); + EXPECT_EQ(standardCancelOutput.errorCode, NOST::EAuctionError::Success); EXPECT_EQ(standardCancelOutput.cancellationFee, 1ULL); EXPECT_EQ(expectedSmallBreakdown.shareholderDividendAmount, 1ULL); EXPECT_EQ(expectedSmallBreakdown.managementFeeAmount, 0ULL); @@ -2144,24 +2360,24 @@ TEST(ContractNostromoAuction, CancelAuctionRejectsAfterBidAuction) EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionId, 1, 12, 12).errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::Success); - const auto notFound = nostromo.cancelAuction(seller, id(800, 0, 0, 0), 10); - EXPECT_EQ(notFound.errorCode, static_cast(NOST::EAuctionError::AuctionNotFound)); + const auto notFound = nostromo.cancelAuction(seller, 800, 10); + EXPECT_EQ(notFound.errorCode, NOST::EAuctionError::AuctionNotFound); - const auto forbidden = nostromo.cancelAuction(bidder, createOutput.auctionId, 10); - EXPECT_EQ(forbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + const auto forbidden = nostromo.cancelAuction(bidder, createOutput.auctionIndex, 10); + EXPECT_EQ(forbidden.errorCode, NOST::EAuctionError::Forbidden); - const auto insufficient = nostromo.cancelAuction(seller, createOutput.auctionId, 1); - EXPECT_EQ(insufficient.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + const auto insufficient = nostromo.cancelAuction(seller, createOutput.auctionIndex, 1); + EXPECT_EQ(insufficient.errorCode, NOST::EAuctionError::Forbidden); - const auto success = nostromo.cancelAuction(seller, createOutput.auctionId, 2); - EXPECT_EQ(success.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + const auto success = nostromo.cancelAuction(seller, createOutput.auctionIndex, 2); + EXPECT_EQ(success.errorCode, NOST::EAuctionError::Forbidden); - const auto closed = nostromo.cancelAuction(seller, createOutput.auctionId, 2); - EXPECT_EQ(closed.errorCode, static_cast(NOST::EAuctionError::Forbidden)); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionId).auction.core.status, NOST::EAuctionStatus::Active); + const auto closed = nostromo.cancelAuction(seller, createOutput.auctionIndex, 2); + EXPECT_EQ(closed.errorCode, NOST::EAuctionError::Forbidden); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); } TEST(ContractNostromoAuction, CancelAuctionRejectsInvalidCasesWithoutBidsAuction) @@ -2176,22 +2392,22 @@ TEST(ContractNostromoAuction, CancelAuctionRejectsInvalidCasesWithoutBidsAuction EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - const auto notFound = nostromo.cancelAuction(seller, id(801, 0, 0, 0), 10); - EXPECT_EQ(notFound.errorCode, static_cast(NOST::EAuctionError::AuctionNotFound)); + const auto notFound = nostromo.cancelAuction(seller, 801, 10); + EXPECT_EQ(notFound.errorCode, NOST::EAuctionError::AuctionNotFound); - const auto forbidden = nostromo.cancelAuction(outsider, createOutput.auctionId, 10); - EXPECT_EQ(forbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + const auto forbidden = nostromo.cancelAuction(outsider, createOutput.auctionIndex, 10); + EXPECT_EQ(forbidden.errorCode, NOST::EAuctionError::Forbidden); - const auto insufficient = nostromo.cancelAuction(seller, createOutput.auctionId, 1); - EXPECT_EQ(insufficient.errorCode, static_cast(NOST::EAuctionError::InsufficientFunds)); + const auto insufficient = nostromo.cancelAuction(seller, createOutput.auctionIndex, 1); + EXPECT_EQ(insufficient.errorCode, NOST::EAuctionError::InsufficientFunds); - const auto success = nostromo.cancelAuction(seller, createOutput.auctionId, 2); - EXPECT_EQ(success.errorCode, static_cast(NOST::EAuctionError::Success)); + const auto success = nostromo.cancelAuction(seller, createOutput.auctionIndex, 2); + EXPECT_EQ(success.errorCode, NOST::EAuctionError::Success); - const auto closed = nostromo.cancelAuction(seller, createOutput.auctionId, 2); - EXPECT_EQ(closed.errorCode, static_cast(NOST::EAuctionError::AuctionClosed)); + const auto closed = nostromo.cancelAuction(seller, createOutput.auctionIndex, 2); + EXPECT_EQ(closed.errorCode, NOST::EAuctionError::AuctionClosed); } TEST(ContractNostromoAuction, ClosedAuctionHistoryRecordsFinalizedAndCancelledAuctionsAuction) @@ -2209,48 +2425,48 @@ TEST(ContractNostromoAuction, ClosedAuctionHistoryRecordsFinalizedAndCancelledAu EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(finalizedSeller, finalizedAsset, 1), 1); const auto finalizedCreateOutput = nostromo.createAuction(finalizedSeller, ContractTestingNOST::makeBatchAuctionInput(finalizedAsset, 1, 10)); - ASSERT_EQ(finalizedCreateOutput.errorCode, static_cast(NOST::EAuctionError::Success)); - ASSERT_EQ(nostromo.placeBid(bidder, finalizedCreateOutput.auctionId, 1, 10, 10).errorCode, - static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(finalizedCreateOutput.errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidder, finalizedCreateOutput.auctionIndex, 1, 10, 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, static_cast(NOST::EAuctionError::Success)); - ASSERT_EQ(nostromo.cancelAuction(cancelledSeller, cancelledCreateOutput.auctionId, 1).errorCode, - static_cast(NOST::EAuctionError::Success)); + 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(containsAuctionId(history.auctionIds, history.totalEntries, finalizedCreateOutput.auctionId)); - EXPECT_TRUE(containsAuctionId(history.auctionIds, history.totalEntries, cancelledCreateOutput.auctionId)); - EXPECT_EQ(nostromo.getAuction(finalizedCreateOutput.auctionId).auction.core.status, NOST::EAuctionStatus::Finalized); - EXPECT_EQ(nostromo.getAuction(cancelledCreateOutput.auctionId).auction.core.status, NOST::EAuctionStatus::Cancelled); + 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 id overwrittenAuctionId(22000, 22001, 22002, 22003); - const id latestAuctionId(23000, 23001, 23002, 23003); + const uint64 overwrittenAuctionIndex = 22000; + const uint64 latestAuctionIndex = 23000; - nostromo.stateData().closedAuctionHistory.set(0, overwrittenAuctionId); + nostromo.stateData().closedAuctionHistory.set(0, overwrittenAuctionIndex); nostromo.stateData().closedAuctionHistoryCounter = 1; for (uint64 index = 1; index < NOST_AUCTION_HISTORY_NUM; ++index) { - nostromo.stateData().closedAuctionHistory.set(index, id(24000 + index, 25000 + index, 26000 + index, 27000 + index)); + nostromo.stateData().closedAuctionHistory.set(index, 24000 + index); ++nostromo.stateData().closedAuctionHistoryCounter; } - nostromo.stateData().closedAuctionHistory.set(0, latestAuctionId); + nostromo.stateData().closedAuctionHistory.set(0, latestAuctionIndex); ++nostromo.stateData().closedAuctionHistoryCounter; const auto history = nostromo.getClosedAuctionHistory(); EXPECT_EQ(history.totalEntries, NOST_AUCTION_HISTORY_NUM + 1ULL); - EXPECT_FALSE(containsAuctionId(history.auctionIds, history.totalEntries, overwrittenAuctionId)); - EXPECT_TRUE(containsAuctionId(history.auctionIds, history.totalEntries, latestAuctionId)); - EXPECT_EQ(history.auctionIds.get(0), latestAuctionId); + EXPECT_FALSE(containsAuctionIndex(history.auctionIndices, history.totalEntries, overwrittenAuctionIndex)); + EXPECT_TRUE(containsAuctionIndex(history.auctionIndices, history.totalEntries, latestAuctionIndex)); + EXPECT_EQ(history.auctionIndices.get(0), latestAuctionIndex); } TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) @@ -2272,15 +2488,15 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) coordinatorInput.shareholderFeeBasisPointsTier4 = 250; const auto coordinatorForbidden = nostromo.setAuctionFees(outsider, coordinatorInput); - EXPECT_EQ(coordinatorForbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + EXPECT_EQ(coordinatorForbidden.errorCode, NOST::EAuctionError::Forbidden); NOST::SetAuctionFees_input invalidCoordinatorInput = coordinatorInput; invalidCoordinatorInput.privateAuctionFee = -1; const auto coordinatorInvalid = nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), invalidCoordinatorInput); - EXPECT_EQ(coordinatorInvalid.errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + EXPECT_EQ(coordinatorInvalid.errorCode, NOST::EAuctionError::InvalidInput); const auto coordinatorSuccess = nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput); - EXPECT_EQ(coordinatorSuccess.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(coordinatorSuccess.errorCode, NOST::EAuctionError::Success); auto fees = nostromo.getAuctionFees(); EXPECT_EQ(fees.privateAuctionFee, 60000000); @@ -2292,13 +2508,13 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) EXPECT_EQ(fees.shareholderFeeBasisPointsTier1, 400ULL); const auto setManagementForbidden = nostromo.setManagement(outsider, newManagement); - EXPECT_EQ(setManagementForbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + EXPECT_EQ(setManagementForbidden.errorCode, NOST::EAuctionError::Forbidden); const auto setManagementInvalid = nostromo.setManagement(ContractTestingNOST::takeoverCoordinatorWallet(), NULL_ID); - EXPECT_EQ(setManagementInvalid.errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + EXPECT_EQ(setManagementInvalid.errorCode, NOST::EAuctionError::InvalidInput); const auto setManagementSuccess = nostromo.setManagement(ContractTestingNOST::takeoverCoordinatorWallet(), newManagement); - EXPECT_EQ(setManagementSuccess.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(setManagementSuccess.errorCode, NOST::EAuctionError::Success); EXPECT_EQ(nostromo.getFeeRecipients().management, newManagement); NOST::SetAuctionFeesByManagement_input managementInput{}; @@ -2312,16 +2528,16 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) managementInput.shareholderFeeBasisPointsTier4 = 150; const auto oldManagementForbidden = nostromo.setAuctionFeesByManagement(ContractTestingNOST::managementWallet(), managementInput); - EXPECT_EQ(oldManagementForbidden.errorCode, static_cast(NOST::EAuctionError::Forbidden)); + EXPECT_EQ(oldManagementForbidden.errorCode, NOST::EAuctionError::Forbidden); NOST::SetAuctionFeesByManagement_input invalidManagementInput = managementInput; invalidManagementInput.managementFeeBasisPoints = 9900; invalidManagementInput.developmentFeeBasisPoints = 200; const auto managementInvalid = nostromo.setAuctionFeesByManagement(newManagement, invalidManagementInput); - EXPECT_EQ(managementInvalid.errorCode, static_cast(NOST::EAuctionError::InvalidInput)); + EXPECT_EQ(managementInvalid.errorCode, NOST::EAuctionError::InvalidInput); const auto managementSuccess = nostromo.setAuctionFeesByManagement(newManagement, managementInput); - EXPECT_EQ(managementSuccess.errorCode, static_cast(NOST::EAuctionError::Success)); + EXPECT_EQ(managementSuccess.errorCode, NOST::EAuctionError::Success); fees = nostromo.getAuctionFees(); EXPECT_EQ(fees.privateAuctionFee, 70000000); @@ -2375,14 +2591,14 @@ TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) const auto createOutput = nostromo.createAuction( seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), cases[caseIndex].grossAmount, cases[caseIndex].grossAmount, 1)); - ASSERT_EQ(createOutput.errorCode, static_cast(NOST::EAuctionError::Success)); + ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); 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.auctionId, 1, cases[caseIndex].grossAmount, cases[caseIndex].grossAmount).errorCode, - static_cast(NOST::EAuctionError::Success)); + 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); From 86f8eb12073bc87e906ae73b7df1bda6c9f88b96 Mon Sep 17 00:00:00 2001 From: N-010 Date: Mon, 25 May 2026 21:40:11 +0300 Subject: [PATCH 38/59] Fix contract spacing, formatting, and minor adjustments in `ContractNostromoAuction` tests for clarity and consistency. --- test/contract_nostromo.cpp | 54 +++++++++++++++----------------------- 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index de249e290..caa482b7d 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -41,6 +41,7 @@ class ContractTestingNOST : protected ContractTesting 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); @@ -145,7 +146,7 @@ class ContractTestingNOST : protected ContractTesting input.quantity = quantity; input.bidAmount = bidAmount; - seedUser(bidder, reward ); + seedUser(bidder, reward); invokeUserProcedure(NOST_CONTRACT_INDEX, 2, input, output, bidder, reward); return output; } @@ -171,7 +172,7 @@ class ContractTestingNOST : protected ContractTesting input.auctionIndex = auctionIndex; if (reward > 0) { - seedUser(seller, reward ); + seedUser(seller, reward); } else { @@ -899,7 +900,8 @@ TEST(ContractNostromoAuction, TransferShareManagementRightsRequiresInvocationRew EXPECT_EQ(nostromo.managedShares(asset, owner), 4); Asset zeroAsset{}; - const auto zeroAssetOutput = nostromo.transferManagedSharesWithReward(owner, zeroAsset, 2, QX_CONTRACT_INDEX, nostromo.getCachedQxTransferFee()); + const auto zeroAssetOutput = + nostromo.transferManagedSharesWithReward(owner, zeroAsset, 2, QX_CONTRACT_INDEX, nostromo.getCachedQxTransferFee()); EXPECT_EQ(zeroAssetOutput.transferredNumberOfShares, 0); EXPECT_EQ(nostromo.managedShares(asset, owner), 4); } @@ -982,8 +984,8 @@ TEST(ContractNostromoAuction, CreateStandardAuctionAcceptsMaximumLotEntriesAucti EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, NOST_AUCTION_LOT_ITEM_NUM), static_cast(NOST_AUCTION_LOT_ITEM_NUM)); - const auto createOutput = nostromo.createAuction( - seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeFullLot(asset, 1), 100, 100, 1)); + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeFullLot(asset, 1), 100, 100, 1)); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); EXPECT_EQ(nostromo.managedShares(asset, seller), 0); EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), static_cast(NOST_AUCTION_LOT_ITEM_NUM)); @@ -1193,8 +1195,7 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacit 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.placeBid(allowedBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, - NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.placeBid(allowedBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, NOST::EAuctionError::Success); } { @@ -1227,8 +1228,7 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacit EXPECT_EQ(auctionOutput.auction.requiredAccessAssetCount, NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM); EXPECT_TRUE(containsAccessAsset(auctionOutput.auction.requiredAccessAssets, auctionOutput.auction.requiredAccessAssetCount, Asset{accessBidder, bidderAccessAssetName})); - EXPECT_EQ(nostromo.placeBid(accessBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, - NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.placeBid(accessBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, NOST::EAuctionError::Success); } } @@ -1361,15 +1361,13 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) auto privateWithoutGate = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); privateWithoutGate.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - EXPECT_EQ(nostromo.createAuction(seller, privateWithoutGate, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, - NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.createAuction(seller, privateWithoutGate, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, NOST::EAuctionError::InvalidInput); auto privateWithBothGates = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); privateWithBothGates.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); privateWithBothGates.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({id(99, 1, 1, 1)}); privateWithBothGates.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({Asset{altIssuer, assetNameFromString("GATINV")}}); - EXPECT_EQ(nostromo.createAuction(seller, privateWithBothGates, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, - NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.createAuction(seller, privateWithBothGates, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, NOST::EAuctionError::InvalidInput); } TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientAssetBalanceAndPauseAuction) @@ -1672,8 +1670,7 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) 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.placeBid(denied, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::PrivateAuctionAccessDenied); EXPECT_EQ(nostromo.placeBid(allowed, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::Success); } @@ -1698,8 +1695,7 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) 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.placeBid(denied, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::PrivateAuctionAccessDenied); EXPECT_EQ(nostromo.placeBid(allowed, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::Success); } } @@ -1839,14 +1835,11 @@ TEST(ContractNostromoAuction, BatchFinalizationRefundsLosingBidsAndTieBreaksByBi const sint64 laterBefore = getBalance(laterBidder); const sint64 lowerBefore = getBalance(lowerBidder); - ASSERT_EQ(nostromo.placeBidWithFundedReward(earlierBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, - NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBidWithFundedReward(earlierBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, NOST::EAuctionError::Success); nostromo.setNow(2026, 1, 1, 9, 0, 1); - ASSERT_EQ(nostromo.placeBidWithFundedReward(laterBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, - NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBidWithFundedReward(laterBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, NOST::EAuctionError::Success); nostromo.setNow(2026, 1, 1, 9, 0, 2); - ASSERT_EQ(nostromo.placeBidWithFundedReward(lowerBidder, createOutput.auctionIndex, 1, 9, 9).errorCode, - NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBidWithFundedReward(lowerBidder, createOutput.auctionIndex, 1, 9, 9).errorCode, NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); @@ -2106,8 +2099,7 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); nostromo.seedUser(bidder, 500); const sint64 bidderBeforeBid = getBalance(bidder); - ASSERT_EQ(nostromo.placeBidWithFundedReward(bidder, createOutput.auctionIndex, 1, 120, 120).errorCode, - NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBidWithFundedReward(bidder, createOutput.auctionIndex, 1, 120, 120).errorCode, NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); const auto rejectOutput = nostromo.resolvePendingStandardAuction(seller, createOutput.auctionIndex, false); @@ -2423,20 +2415,16 @@ TEST(ContractNostromoAuction, ClosedAuctionHistoryRecordsFinalizedAndCancelledAu 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)); + const auto finalizedCreateOutput = nostromo.createAuction(finalizedSeller, ContractTestingNOST::makeBatchAuctionInput(finalizedAsset, 1, 10)); ASSERT_EQ(finalizedCreateOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(bidder, finalizedCreateOutput.auctionIndex, 1, 10, 10).errorCode, - NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidder, finalizedCreateOutput.auctionIndex, 1, 10, 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)); + 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); + ASSERT_EQ(nostromo.cancelAuction(cancelledSeller, cancelledCreateOutput.auctionIndex, 1).errorCode, NOST::EAuctionError::Success); const auto history = nostromo.getClosedAuctionHistory(); EXPECT_EQ(history.totalEntries, 2ULL); From a28f4c6a6724b294899a9d045214d18a3b58ee4f Mon Sep 17 00:00:00 2001 From: N-010 Date: Mon, 25 May 2026 21:54:14 +0300 Subject: [PATCH 39/59] Update `allowedBidder` values in `ContractNostromoAuction` tests to reflect revised wallet IDs. --- test/contract_nostromo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index caa482b7d..5243bbf83 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -1173,7 +1173,7 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacit { ContractTestingNOST nostromo; const id seller(47, 48, 49, 50); - const id allowedBidder(30127, 31127, 32127, 33127); + const id allowedBidder(30007, 31007, 32007, 33007); const uint64 assetName = assetNameFromString("MAXWAL"); const Asset asset{seller, assetName}; Array allowedWallets{}; From 4408dc091c395b465b1cfab55ac7c82ff62e8f31 Mon Sep 17 00:00:00 2001 From: N-010 Date: Mon, 25 May 2026 22:41:11 +0300 Subject: [PATCH 40/59] Add error code handling to `transferManagedShares` and auction methods. Update tests and logging to validate outputs and ensure consistent error propagation. --- src/contracts/Nostromo.h | 139 +++++++++++++++++++++++-------------- test/contract_nostromo.cpp | 29 ++++++++ 2 files changed, 117 insertions(+), 51 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 33f0fe617..b2ada8197 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -85,7 +85,8 @@ struct NOST : public ContractBase InvalidVisibility, BidTooLow, PrivateAuctionAccessDenied, - AuctionPaused + AuctionPaused, + AuctionIndexExhausted }; struct AuctionParticipantKey @@ -1566,6 +1567,9 @@ struct NOST : public ContractBase { /** @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 @@ -2756,7 +2760,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::AuctionPaused; setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -2768,7 +2772,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::InvalidAuctionType; setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -2780,7 +2784,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::InvalidVisibility; setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -2792,7 +2796,19 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::StorageFull; setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); - LOG_INFO(locals.log); + 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(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + logProcedureResult(locals.log); return; } @@ -2804,8 +2820,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + output.errorCode = EAuctionError::InvalidInput; setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -2818,8 +2835,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + output.errorCode = EAuctionError::InvalidInput; setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } locals.resolvedQuantityForSale = 0; @@ -2835,8 +2853,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + output.errorCode = EAuctionError::InvalidInput; setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } break; @@ -2849,8 +2868,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + output.errorCode = EAuctionError::InvalidInput; setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } break; @@ -2861,7 +2881,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::InvalidAuctionType; setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -2877,8 +2897,9 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } + output.errorCode = EAuctionError::InvalidInput; setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -2891,7 +2912,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::InsufficientFunds; setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -2905,7 +2926,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::InsufficientAssetBalance; setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -2919,7 +2940,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::InsufficientAssetBalance; setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -2966,7 +2987,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::StorageFull; setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -2982,7 +3003,7 @@ struct NOST : public ContractBase state.mut().totalAuctionsCreated = sadd(state.get().totalAuctionsCreated, 1ULL); output.errorCode = EAuctionError::Success; setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); - LOG_INFO(locals.log); + logProcedureResult(locals.log); } /** @@ -3003,7 +3024,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::AuctionPaused; setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3015,7 +3036,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::AuctionNotFound; setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3027,7 +3048,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::AuctionClosed; setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3039,7 +3060,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::Forbidden; setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3053,7 +3074,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::AuctionClosed; setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3078,7 +3099,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::PrivateAuctionAccessDenied; setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } } @@ -3100,7 +3121,7 @@ struct NOST : public ContractBase } output.errorCode = locals.processBatchBidOutput.errorCode; setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } output.refundedAmount = sadd(output.refundedAmount, locals.processBatchBidOutput.refundedAmount); @@ -3120,7 +3141,7 @@ struct NOST : public ContractBase } output.errorCode = locals.processStandardBidOutput.errorCode; setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } output.refundedAmount = sadd(output.refundedAmount, locals.processStandardBidOutput.refundedAmount); @@ -3133,12 +3154,12 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::InvalidAuctionType; setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } output.errorCode = EAuctionError::Success; setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); } /** @@ -3161,7 +3182,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::AuctionNotFound; setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3173,7 +3194,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::AuctionClosed; setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3185,7 +3206,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::Forbidden; setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3197,7 +3218,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::Forbidden; setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3216,7 +3237,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::InsufficientFunds; setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3259,7 +3280,7 @@ struct NOST : public ContractBase output.errorCode = EAuctionError::Success; setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); - LOG_INFO(locals.log); + logProcedureResult(locals.log); } /** @@ -3281,14 +3302,15 @@ struct NOST : public ContractBase { output.errorCode = EAuctionError::AuctionPaused; setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } if (input.acceptSale > 1) { + output.errorCode = EAuctionError::InvalidInput; setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3296,7 +3318,7 @@ struct NOST : public ContractBase { output.errorCode = EAuctionError::AuctionNotFound; setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3304,7 +3326,7 @@ struct NOST : public ContractBase { output.errorCode = EAuctionError::Forbidden; setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3312,7 +3334,7 @@ struct NOST : public ContractBase { output.errorCode = EAuctionError::AuctionClosed; setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3325,7 +3347,7 @@ struct NOST : public ContractBase output.errorCode = EAuctionError::AuctionClosed; setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3345,7 +3367,7 @@ struct NOST : public ContractBase output.errorCode = locals.rejectStandardAuctionOutput.success ? EAuctionError::Success : EAuctionError::AuctionClosed; } setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); - LOG_INFO(locals.log); + logProcedureResult(locals.log); } /** @@ -3364,7 +3386,7 @@ struct NOST : public ContractBase { output.errorCode = EAuctionError::Forbidden; setProcedureLogInput(locals.log, qpi.invocator(), 6, output.errorCode, 0, 0); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3375,7 +3397,7 @@ struct NOST : public ContractBase { output.errorCode = EAuctionError::InvalidInput; setProcedureLogInput(locals.log, qpi.invocator(), 6, output.errorCode, 0, 0); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3391,7 +3413,7 @@ struct NOST : public ContractBase state.mut().shareholderFeeBasisPointsTier4 = input.shareholderFeeBasisPointsTier4; output.errorCode = EAuctionError::Success; setProcedureLogInput(locals.log, qpi.invocator(), 6, output.errorCode, 0, 0); - LOG_INFO(locals.log); + logProcedureResult(locals.log); } /** @@ -3411,7 +3433,7 @@ struct NOST : public ContractBase { output.errorCode = EAuctionError::Forbidden; setProcedureLogInput(locals.log, qpi.invocator(), 7, output.errorCode, 0, 0); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3420,8 +3442,9 @@ struct NOST : public ContractBase state.get().takeoverCoordinatorFeeBasisPoints, state.get().shareholderDividendBasisPoints, input.shareholderFeeBasisPointsTier1, input.shareholderFeeBasisPointsTier2, input.shareholderFeeBasisPointsTier3, input.shareholderFeeBasisPointsTier4)) { + output.errorCode = EAuctionError::InvalidInput; setProcedureLogInput(locals.log, qpi.invocator(), 7, output.errorCode, 0, 0); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } @@ -3435,7 +3458,7 @@ struct NOST : public ContractBase state.mut().shareholderFeeBasisPointsTier4 = input.shareholderFeeBasisPointsTier4; output.errorCode = EAuctionError::Success; setProcedureLogInput(locals.log, qpi.invocator(), 7, output.errorCode, 0, 0); - LOG_INFO(locals.log); + logProcedureResult(locals.log); } /** @@ -3455,21 +3478,22 @@ struct NOST : public ContractBase { output.errorCode = EAuctionError::Forbidden; setProcedureLogInput(locals.log, qpi.invocator(), 8, output.errorCode, 0, 0); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } if (isZero(input.management)) { + output.errorCode = EAuctionError::InvalidInput; setProcedureLogInput(locals.log, qpi.invocator(), 8, output.errorCode, 0, 0); - LOG_INFO(locals.log); + logProcedureResult(locals.log); return; } state.mut().management = input.management; output.errorCode = EAuctionError::Success; setProcedureLogInput(locals.log, qpi.invocator(), 8, output.errorCode, 0, 0); - LOG_INFO(locals.log); + logProcedureResult(locals.log); } /** @@ -3818,6 +3842,7 @@ struct NOST : public ContractBase locals.refundAmount = locals.reward; locals.success = false; output.transferredNumberOfShares = 0; + output.errorCode = EAuctionError::InvalidInput; if (input.numberOfShares > 0 && qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) >= input.numberOfShares) @@ -3834,19 +3859,31 @@ struct NOST : public ContractBase 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(), 4, locals.success ? EAuctionError::Success : EAuctionError::InvalidInput, 0, - output.transferredNumberOfShares); + setProcedureLogInput(locals.log, qpi.invocator(), 4, output.errorCode, 0, output.transferredNumberOfShares); - LOG_INFO(locals.log); + logProcedureResult(locals.log); } protected: + static void logProcedureResult(const NostromoProcedureLog& log) + { + if (log.errorCode == EAuctionError::Success) + { + LOG_INFO(log); + } + else + { + LOG_ERROR(log); + } + } + static void setProcedureLogInput(NostromoProcedureLog& log, const id& actor, uint8 procedure, EAuctionError errorCode, uint64 auctionIndex, sint64 amount) { diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index 5243bbf83..bacd2db07 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -814,19 +814,24 @@ TEST(ContractNostromoAuction, TransferShareManagementRightsAuction) 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); } @@ -845,6 +850,7 @@ TEST(ContractNostromoAuction, TransferShareManagementRightsRequiresInvocationRew 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); } @@ -861,6 +867,7 @@ TEST(ContractNostromoAuction, TransferShareManagementRightsRequiresInvocationRew 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); } @@ -880,6 +887,7 @@ TEST(ContractNostromoAuction, TransferShareManagementRightsRequiresInvocationRew 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); @@ -897,12 +905,14 @@ TEST(ContractNostromoAuction, TransferShareManagementRightsRequiresInvocationRew 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); } } @@ -1470,6 +1480,25 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsWhenAuctionStorageIsFullAuctio 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; From 618f10c2ea9b2e13cb80215635204acbad0fbea4 Mon Sep 17 00:00:00 2001 From: N-010 Date: Tue, 26 May 2026 19:30:01 +0300 Subject: [PATCH 41/59] Changes to the order of fields in NostromoProcedureLog --- src/contracts/Nostromo.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index b2ada8197..d3e94ccf9 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -963,13 +963,12 @@ struct NOST : public ContractBase struct NostromoProcedureLog { + uint32 contractIndex; + EAuctionError errorCode; id actor; sint64 amount; uint64 auctionIndex; - uint32 contractIndex; uint8 procedure; - EAuctionError errorCode; - sint8 _terminator; }; From dbbb00ae2efdb27ca078cc227a588fa78ffb0440 Mon Sep 17 00:00:00 2001 From: N-010 Date: Tue, 26 May 2026 19:42:12 +0300 Subject: [PATCH 42/59] Changes the field type from EAuctionError to uint32 in NostromoProcedureLog --- src/contracts/Nostromo.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index d3e94ccf9..68b6b1297 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -964,7 +964,7 @@ struct NOST : public ContractBase struct NostromoProcedureLog { uint32 contractIndex; - EAuctionError errorCode; + uint32 errorCode; id actor; sint64 amount; uint64 auctionIndex; @@ -3873,7 +3873,7 @@ struct NOST : public ContractBase protected: static void logProcedureResult(const NostromoProcedureLog& log) { - if (log.errorCode == EAuctionError::Success) + if (log.errorCode == static_cast(EAuctionError::Success)) { LOG_INFO(log); } @@ -3888,7 +3888,7 @@ struct NOST : public ContractBase { log.contractIndex = SELF_INDEX; log.procedure = procedure; - log.errorCode = errorCode; + log.errorCode = static_cast(errorCode); log.auctionIndex = auctionIndex; log.actor = actor; log.amount = amount; From 7a4005cde9c35819f1f6e4d6cd642b9f96511d92 Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 10 Jun 2026 21:40:00 +0300 Subject: [PATCH 43/59] The wallet whitelist has been expanded, each lot is limited to a single asset, and asset-based access now takes into account the minimum required quantity and validates the input data. --- src/contracts/Nostromo.h | 117 +++++++++++--------- test/contract_nostromo.cpp | 211 +++++++++++++++++++------------------ 2 files changed, 181 insertions(+), 147 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 68b6b1297..bb60161ec 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -13,8 +13,8 @@ constexpr uint64 NOST_AUCTION_HISTORY_NUM = 1024; constexpr uint64 NOST_AUCTION_METADATA_CID_LENGTH = 64; constexpr uint64 NOST_AUCTION_PARTICIPANT_NUM = 4096; constexpr uint64 NOST_AUCTION_GETTER_PAGE_SIZE = 64; -constexpr uint64 NOST_AUCTION_LOT_ITEM_NUM = 8; -constexpr uint64 NOST_AUCTION_ALLOWED_WALLET_NUM = 8; +constexpr uint64 NOST_AUCTION_LOT_ITEM_NUM = 1; +constexpr uint64 NOST_AUCTION_ALLOWED_WALLET_NUM = 16; constexpr uint64 NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM = 4; constexpr uint32 NOST_AUCTION_MAX_DURATION_DAYS = 30; constexpr sint64 NOST_DEFAULT_PRIVATE_AUCTION_FEE = 50000000LL; @@ -126,16 +126,14 @@ struct NOST : public ContractBase }; /** - * @brief Describes one asset entry inside an auction lot. - * @note One non-zero entry means a single-asset auction lot. - * @note Multiple non-zero entries mean a bundle of different assets. + * @brief Describes an asset and quantity used by an auction lot or private access rule. */ - struct AuctionLotEntry + struct AuctionAssetEntry { - /** @brief Asset included in the auction lot. */ + /** @brief Asset included in a lot or used as an access requirement. */ Asset asset; - /** @brief Quantity of this asset included in the auction lot. */ + /** @brief Lot quantity or minimum ownership quantity required for access. */ sint64 quantity; }; @@ -147,8 +145,8 @@ struct NOST : public ContractBase */ struct AuctionCore { - /** @brief Auction lot contents; one non-empty entry means a single asset, multiple non-empty entries mean a bundle. */ - Array auctionLotItems; + /** @brief Single asset and quantity offered by the auction. */ + Array auctionLotItems; /** @brief Lowercase base32 CIDv1 stored in Pinata for the auction name and description metadata. */ Array metadataIpfsCid; @@ -230,8 +228,8 @@ struct NOST : public ContractBase /** @brief Wallet whitelist used when the private auction uses wallet-based access. */ HashSet allowedBidderWallets; - /** @brief Asset set required for participation when the private auction uses asset-based access. */ - HashSet requiredAccessAssets; + /** @brief Minimum quantity by asset required for participation; owning any one entry grants access. */ + HashMap requiredAccessAssets; }; /** @@ -247,8 +245,8 @@ struct NOST : public ContractBase /** @brief Wallet list used when the private auction restricts participation to predefined wallets. */ Array allowedBidderWallets; - /** @brief Asset list required to participate when the private auction uses asset-based access. */ - Array requiredAccessAssets; + /** @brief Asset and minimum-quantity alternatives used by private asset-based access. */ + Array requiredAccessAssets; /** @brief Number of populated entries in `requiredAccessAssets`. */ uint64 requiredAccessAssetCount; @@ -338,11 +336,11 @@ struct NOST : public ContractBase /** @brief Lowercase base32 CIDv1 stored in Pinata for the auction name and description metadata. */ Array metadataIpfsCid; - /** @brief Auction lot contents; one non-empty entry means a single asset, multiple non-empty entries mean a bundle. */ - Array auctionLotItems; + /** @brief Single asset and quantity offered by the auction. */ + Array auctionLotItems; - /** @brief Asset list required to participate when the private auction uses asset-based access. */ - Array requiredAccessAssets; + /** @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; @@ -897,7 +895,7 @@ struct NOST : public ContractBase struct AnalyzeAuctionLot_input { /** @brief Auction lot contents to validate. */ - Array auctionLotItems; + Array auctionLotItems; /** @brief Requested auction duration in days. */ uint32 durationDays; @@ -918,7 +916,7 @@ struct NOST : public ContractBase struct AnalyzeAuctionLot_locals { - AuctionLotEntry lotItem; + AuctionAssetEntry lotItem; uint64 lotItemIndex; }; @@ -944,20 +942,23 @@ struct NOST : public ContractBase /** @brief Internal input used to count non-empty asset entries in a private asset access list. */ struct CountRequiredAccessAssets_input { - /** @brief Asset list provided for private asset-based access control. */ - Array requiredAccessAssets; + /** @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 non-zero asset entries found in the private access list. */ + /** @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 { - Asset requiredAccessAsset; + AuctionAssetEntry requiredAccessAsset; uint64 requiredAccessAssetIndex; }; @@ -975,7 +976,7 @@ struct NOST : public ContractBase struct GetAuctionByIndex_locals { AuctionData auction; - Asset requiredAccessAsset; + AuctionAssetEntry requiredAccessAsset; id allowedBidderWallet; sint64 requiredAccessAssetSetIndex; sint64 allowedBidderWalletSetIndex; @@ -1008,7 +1009,7 @@ struct NOST : public ContractBase using GetAuctionCountBySeller_locals = GetterScan_locals; using GetAuctionAtCreationSnapshot_locals = GetterScan_locals; - /** @brief Internal input used to verify whether the invocator owns at least one required private access asset. */ + /** @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. */ @@ -1018,14 +1019,14 @@ struct NOST : public ContractBase /** @brief Internal output of the private asset access check. */ struct HasRequiredAccessAsset_output { - /** @brief Flag indicating whether the invocator owns at least one required access asset. */ + /** @brief Flag indicating whether the invocator owns the minimum quantity of any required asset. */ uint8 hasRequiredAccessAsset; }; struct HasRequiredAccessAsset_locals { AuctionData auction; - Asset requiredAccessAsset; + AuctionAssetEntry requiredAccessAsset; sint64 requiredAccessAssetSetIndex; sint64 possessedAccessShares; }; @@ -1335,7 +1336,7 @@ struct NOST : public ContractBase struct VerifyAuctionLotBalances_input { /** @brief Auction lot that should be checked against the seller balance. */ - Array auctionLotItems; + Array auctionLotItems; }; /** @brief Internal output of the seller balance verification routine. */ @@ -1347,7 +1348,7 @@ struct NOST : public ContractBase struct VerifyAuctionLotBalances_locals { - AuctionLotEntry lotItem; + AuctionAssetEntry lotItem; uint64 lotItemIndex; sint64 possessedShares; }; @@ -1356,7 +1357,7 @@ struct NOST : public ContractBase struct EscrowAuctionLotAssets_input { /** @brief Auction lot that must be moved into contract escrow. */ - Array auctionLotItems; + Array auctionLotItems; }; /** @brief Internal output of the lot escrow routine. */ @@ -1368,7 +1369,7 @@ struct NOST : public ContractBase struct EscrowAuctionLotAssets_locals { - AuctionLotEntry lotItem; + AuctionAssetEntry lotItem; uint64 lotItemIndex; uint64 rollbackLotItemIndex; sint64 remainingShares; @@ -1378,7 +1379,7 @@ struct NOST : public ContractBase struct RollbackAuctionLotAssets_input { /** @brief Auction lot that must be transferred out of contract escrow. */ - Array auctionLotItems; + Array auctionLotItems; /** @brief Destination wallet that should receive the lot from escrow. */ id recipient; @@ -1388,7 +1389,7 @@ struct NOST : public ContractBase struct RollbackAuctionLotAssets_locals { - AuctionLotEntry lotItem; + AuctionAssetEntry lotItem; uint64 lotItemIndex; }; @@ -1399,7 +1400,7 @@ struct NOST : public ContractBase AuctionParticipantData bestParticipantData; AuctionParticipantKey participantKey; AuctionParticipantKey bestParticipantKey; - AuctionLotEntry batchLotItem; + AuctionAssetEntry batchLotItem; DistributeAuctionRevenue_input distributeAuctionRevenueInput; DistributeAuctionRevenue_output distributeAuctionRevenueOutput; DateAndTime currentDate; @@ -1466,11 +1467,13 @@ struct NOST : public ContractBase CountAllowedBidderWallets_output countAllowedBidderWalletsOutput; CountRequiredAccessAssets_input countRequiredAccessAssetsInput; CountRequiredAccessAssets_output countRequiredAccessAssetsOutput; + AuctionAssetEntry requiredAccessAsset; VerifyAuctionLotBalances_input verifyAuctionLotBalancesInput; EscrowAuctionLotAssets_input escrowAuctionLotAssetsInput; RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; DistributeAuctionServiceFee_input distributeAuctionServiceFeeInput; sint64 requiredFee; + sint64 existingRequiredAccessQuantity; uint64 resolvedQuantityForSale; uint64 resolvedMinimumPurchaseQuantity; uint64 allowedWalletIndex; @@ -2079,14 +2082,28 @@ struct NOST : public ContractBase PRIVATE_FUNCTION_WITH_LOCALS(CountRequiredAccessAssets) { output.requiredAccessAssetCount = 0; + output.isValid = 1; for (locals.requiredAccessAssetIndex = 0; locals.requiredAccessAssetIndex < input.requiredAccessAssets.capacity(); ++locals.requiredAccessAssetIndex) { locals.requiredAccessAsset = input.requiredAccessAssets.get(locals.requiredAccessAssetIndex); - if (!isZeroAsset(locals.requiredAccessAsset)) + if (isZeroAsset(locals.requiredAccessAsset.asset)) { - output.requiredAccessAssetCount = sadd(output.requiredAccessAssetCount, 1ULL); + 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); } } @@ -2102,10 +2119,11 @@ struct NOST : public ContractBase locals.requiredAccessAssetSetIndex != NULL_INDEX; locals.requiredAccessAssetSetIndex = locals.auction.requiredAccessAssets.nextElementIndex(locals.requiredAccessAssetSetIndex)) { - locals.requiredAccessAsset = locals.auction.requiredAccessAssets.key(locals.requiredAccessAssetSetIndex); - locals.possessedAccessShares = qpi.numberOfShares(locals.requiredAccessAsset, AssetOwnershipSelect::byOwner(qpi.invocator()), + 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 > 0) + if (locals.possessedAccessShares >= locals.requiredAccessAsset.quantity) { output.hasRequiredAccessAsset = 1; return; @@ -2888,7 +2906,8 @@ struct NOST : public ContractBase CALL(CountAllowedBidderWallets, locals.countAllowedBidderWalletsInput, locals.countAllowedBidderWalletsOutput); locals.countRequiredAccessAssetsInput.requiredAccessAssets = input.requiredAccessAssets; CALL(CountRequiredAccessAssets, locals.countRequiredAccessAssetsInput, locals.countRequiredAccessAssetsOutput); - if (!validatePrivateAuctionAccess(static_cast(input.auctionVisibility), + if (!locals.countRequiredAccessAssetsOutput.isValid || + !validatePrivateAuctionAccess(static_cast(input.auctionVisibility), locals.countRequiredAccessAssetsOutput.requiredAccessAssetCount, locals.countAllowedBidderWalletsOutput.allowedWalletCount)) { @@ -2957,9 +2976,12 @@ struct NOST : public ContractBase for (locals.requiredAccessAssetIndex = 0; locals.requiredAccessAssetIndex < input.requiredAccessAssets.capacity(); ++locals.requiredAccessAssetIndex) { - if (!isZeroAsset(input.requiredAccessAssets.get(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.add(input.requiredAccessAssets.get(locals.requiredAccessAssetIndex)); + locals.auction.requiredAccessAssets.set(locals.requiredAccessAsset.asset, locals.requiredAccessAsset.quantity); } } locals.auction.core.auctionLotItems = input.auctionLotItems; @@ -3497,7 +3519,7 @@ struct NOST : public ContractBase /** * @brief Returns the stored state of one auction. - * @note The response contains a serializable auction view; access-control sets are returned as fixed arrays with counts. + * @note The response contains a serializable auction view; access-control containers are returned as fixed arrays with counts. */ PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionByIndex) { @@ -3515,7 +3537,8 @@ struct NOST : public ContractBase locals.requiredAccessAssetSetIndex != NULL_INDEX; locals.requiredAccessAssetSetIndex = locals.auction.requiredAccessAssets.nextElementIndex(locals.requiredAccessAssetSetIndex)) { - locals.requiredAccessAsset = locals.auction.requiredAccessAssets.key(locals.requiredAccessAssetSetIndex); + 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); } @@ -3989,12 +4012,12 @@ struct NOST : public ContractBase return true; } - static bool validatePrivateAuctionAccess(EAuctionVisibility visibility, uint64 requiredAccessAssetCount, uint64 allowedWalletCount) + constexpr static bool validatePrivateAuctionAccess(EAuctionVisibility visibility, uint64 requiredAccessAssetCount, uint64 allowedWalletCount) { return visibility != EAuctionVisibility::Private || ((requiredAccessAssetCount > 0) != (allowedWalletCount > 0)); } - static bool isValidAuctionFeeConfiguration(sint64 privateAuctionFee, uint64 auctionCancellationFeeBasisPoints, uint64 managementFeeBasisPoints, + constexpr static bool isValidAuctionFeeConfiguration(sint64 privateAuctionFee, uint64 auctionCancellationFeeBasisPoints, uint64 managementFeeBasisPoints, uint64 developmentFeeBasisPoints, uint64 takeoverCoordinatorFeeBasisPoints, uint64 shareholderDividendBasisPoints, uint64 shareholderFeeBasisPointsTier1, uint64 shareholderFeeBasisPointsTier2, uint64 shareholderFeeBasisPointsTier3, diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index bacd2db07..c11114c90 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -7,6 +7,7 @@ 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 @@ -109,6 +110,21 @@ class ContractTestingNOST : protected ContractTesting 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{}; @@ -489,10 +505,10 @@ class ContractTestingNOST : protected ContractTesting return cid; } - static Array makeSingleLot(const Asset& asset, sint64 quantity) + static Array makeSingleLot(const Asset& asset, sint64 quantity) { - Array lot{}; - NOST::AuctionLotEntry entry{}; + Array lot{}; + NOST::AuctionAssetEntry entry{}; entry.asset = asset; entry.quantity = quantity; @@ -500,22 +516,6 @@ class ContractTestingNOST : protected ContractTesting return lot; } - static Array makeTwoAssetLot(const Asset& assetA, sint64 quantityA, const Asset& assetB, - sint64 quantityB) - { - Array lot{}; - NOST::AuctionLotEntry entryA{}; - NOST::AuctionLotEntry entryB{}; - - entryA.asset = assetA; - entryA.quantity = quantityA; - entryB.asset = assetB; - entryB.quantity = quantityB; - lot.set(0, entryA); - lot.set(1, entryB); - return lot; - } - static Array makeAllowedWallets(std::initializer_list wallets) { Array allowed{}; @@ -527,9 +527,10 @@ class ContractTestingNOST : protected ContractTesting return allowed; } - static Array makeRequiredAccessAssets(std::initializer_list assets) + static Array + makeRequiredAccessAssets(std::initializer_list assets) { - Array required{}; + Array required{}; uint64 index = 0; for (const auto& asset : assets) { @@ -538,19 +539,6 @@ class ContractTestingNOST : protected ContractTesting return required; } - static Array makeFullLot(const Asset& asset, sint64 quantity) - { - Array lot{}; - for (uint64 index = 0; index < lot.capacity(); ++index) - { - NOST::AuctionLotEntry entry{}; - entry.asset = asset; - entry.quantity = quantity; - lot.set(index, entry); - } - return lot; - } - static NOST::CreateAuction_input makeBatchAuctionInput(const Asset& asset, sint64 quantity, uint64 salePrice = 10) { NOST::CreateAuction_input input{}; @@ -563,7 +551,7 @@ class ContractTestingNOST : protected ContractTesting return input; } - static NOST::CreateAuction_input makeStandardAuctionInput(const Array& lot, + static NOST::CreateAuction_input makeStandardAuctionInput(const Array& lot, uint64 initialPrice = 100, uint64 salePrice = 150, uint64 minimumBidIncrement = 10, uint64 buyNowPrice = 0) { @@ -631,11 +619,12 @@ static bool containsWallet(const Array& wal return false; } -static bool containsAccessAsset(const Array& assets, uint64 count, const Asset& asset) +static bool containsAccessAsset(const Array& assets, uint64 count, + const NOST::AuctionAssetEntry& expected) { for (uint64 index = 0; index < count; ++index) { - if (assets.get(index) == asset) + if (assets.get(index).asset == expected.asset && assets.get(index).quantity == expected.quantity) { return true; } @@ -948,21 +937,17 @@ TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 9); } -TEST(ContractNostromoAuction, CreateStandardBundleAuctionEscrowsMixedLotAuction) +TEST(ContractNostromoAuction, CreateStandardSingleAssetAuctionEscrowsLotAuction) { ContractTestingNOST nostromo; const id seller(21, 22, 23, 24); - const uint64 assetNameA = assetNameFromString("CRTSTA"); - const uint64 assetNameB = assetNameFromString("CRTSTB"); - const Asset assetA{seller, assetNameA}; - const Asset assetB{seller, assetNameB}; + const uint64 assetName = assetNameFromString("CRTSTA"); + const Asset asset{seller, assetName}; - EXPECT_EQ(nostromo.issueAsset(seller, assetNameA, 2), 2); - EXPECT_EQ(nostromo.issueAsset(seller, assetNameB, 3), 3); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetA, 2), 2); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetB, 3), 3); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 5), 5); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 5), 5); - auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeTwoAssetLot(assetA, 2, assetB, 3), 100, 150, 5); + auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 5), 100, 150, 5); const auto output = nostromo.createAuction(seller, input); ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); @@ -974,15 +959,12 @@ TEST(ContractNostromoAuction, CreateStandardBundleAuctionEscrowsMixedLotAuction) EXPECT_EQ(auction.core.salePrice, 150ULL); EXPECT_EQ(auction.core.minimumBidIncrement, 5ULL); EXPECT_EQ(auction.core.type, NOST::EAuctionType::Standard); - EXPECT_EQ(auction.core.auctionLotItems.get(0).asset, assetA); - EXPECT_EQ(auction.core.auctionLotItems.get(0).quantity, 2); - EXPECT_EQ(auction.core.auctionLotItems.get(1).asset, assetB); - EXPECT_EQ(auction.core.auctionLotItems.get(1).quantity, 3); - EXPECT_EQ(nostromo.sharesManagedBy(assetA, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 2); - EXPECT_EQ(nostromo.sharesManagedBy(assetB, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 3); + 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, CreateStandardAuctionAcceptsMaximumLotEntriesAuction) +TEST(ContractNostromoAuction, CreateStandardAuctionSupportsSingleLotEntryAuction) { ContractTestingNOST nostromo; const id seller(25, 26, 27, 28); @@ -990,22 +972,22 @@ TEST(ContractNostromoAuction, CreateStandardAuctionAcceptsMaximumLotEntriesAucti const uint64 assetName = assetNameFromString("MAXLOT"); const Asset asset{seller, assetName}; - EXPECT_EQ(nostromo.issueAsset(seller, assetName, NOST_AUCTION_LOT_ITEM_NUM), static_cast(NOST_AUCTION_LOT_ITEM_NUM)); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, NOST_AUCTION_LOT_ITEM_NUM), - static_cast(NOST_AUCTION_LOT_ITEM_NUM)); + EXPECT_EQ(NOST_AUCTION_LOT_ITEM_NUM, 1ULL); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); - const auto createOutput = - nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeFullLot(asset, 1), 100, 100, 1)); + const auto createOutput = nostromo.createAuction( + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 3), 100, 100, 1)); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); EXPECT_EQ(nostromo.managedShares(asset, seller), 0); - EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), static_cast(NOST_AUCTION_LOT_ITEM_NUM)); + EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 3); ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, 100, 100).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(nostromo.managedShares(asset, bidder), static_cast(NOST_AUCTION_LOT_ITEM_NUM)); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 3); EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 0); } @@ -1050,7 +1032,7 @@ TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction auto input = ContractTestingNOST::makeBatchAuctionInput(saleAsset, 5, 20); input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({gateAsset}); + 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); @@ -1059,7 +1041,8 @@ TEST(ContractNostromoAuction, CreatePrivateAuctionsByWalletAndAccessAssetAuction EXPECT_EQ(auction.core.visibility, NOST::EAuctionVisibility::Private); EXPECT_EQ(auction.allowedBidderWalletCount, 0U); EXPECT_EQ(auction.requiredAccessAssetCount, 1U); - EXPECT_EQ(auction.requiredAccessAssets.get(0), gateAsset); + EXPECT_EQ(auction.requiredAccessAssets.get(0).asset, gateAsset); + EXPECT_EQ(auction.requiredAccessAssets.get(0).quantity, 1); EXPECT_GT(nostromo.plainShares(gateAsset, gatedBidder), 0); } } @@ -1108,7 +1091,8 @@ TEST(ContractNostromoAuction, GetAuctionViewExposesAccessListsAndFoundFlagAuctio auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10); input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({accessAssetA, accessAssetB}); + 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); @@ -1116,8 +1100,10 @@ TEST(ContractNostromoAuction, GetAuctionViewExposesAccessListsAndFoundFlagAuctio 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, accessAssetA)); - EXPECT_TRUE(containsAccessAsset(auctionOutput.auction.requiredAccessAssets, auctionOutput.auction.requiredAccessAssetCount, accessAssetB)); + 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); } @@ -1167,14 +1153,16 @@ TEST(ContractNostromoAuction, GetAuctionViewDeduplicatesPrivateAccessInputsAucti auto input = ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10); input.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - input.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({accessAsset, accessAsset}); + 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, accessAsset)); + EXPECT_TRUE(containsAccessAsset(auction.requiredAccessAssets, auction.requiredAccessAssetCount, + NOST::AuctionAssetEntry{accessAsset, 5})); } } @@ -1187,6 +1175,7 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacit 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) { @@ -1216,13 +1205,14 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacit const uint64 saleAssetName = assetNameFromString("MAXACC"); const uint64 bidderAccessAssetName = assetNameFromString("MAXACB"); const Asset saleAsset{seller, saleAssetName}; - Array requiredAssets{}; + Array requiredAssets{}; for (uint64 index = 0; index < NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM; ++index) { - requiredAssets.set(index, Asset{gateIssuer, 34000 + index}); + requiredAssets.set(index, NOST::AuctionAssetEntry{Asset{gateIssuer, 34000 + index}, 1}); } - requiredAssets.set(NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM - 1, Asset{accessBidder, bidderAccessAssetName}); + 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); @@ -1237,7 +1227,7 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacit 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, - Asset{accessBidder, bidderAccessAssetName})); + NOST::AuctionAssetEntry{Asset{accessBidder, bidderAccessAssetName}, 1})); EXPECT_EQ(nostromo.placeBid(accessBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, NOST::EAuctionError::Success); } } @@ -1301,14 +1291,11 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) const id seller(51, 52, 53, 54); const id altIssuer(55, 56, 57, 58); const uint64 assetNameA = assetNameFromString("INVAAA"); - const uint64 assetNameB = assetNameFromString("INVBBB"); const Asset assetA{seller, assetNameA}; - const Asset assetB{seller, assetNameB}; + const Asset accessAsset{altIssuer, assetNameFromString("GATINV")}; EXPECT_EQ(nostromo.issueAsset(seller, assetNameA, 5), 5); - EXPECT_EQ(nostromo.issueAsset(seller, assetNameB, 5), 5); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetA, 5), 5); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetB, 5), 5); EXPECT_EQ(nostromo.issueAsset(altIssuer, assetNameFromString("GATINV"), 1), 1); auto invalidCid = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); @@ -1320,7 +1307,7 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) EXPECT_EQ(nostromo.createAuction(seller, invalidCidUppercase).errorCode, NOST::EAuctionError::InvalidInput); auto emptyLot = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - emptyLot.auctionLotItems = Array{}; + emptyLot.auctionLotItems = Array{}; EXPECT_EQ(nostromo.createAuction(seller, emptyLot).errorCode, NOST::EAuctionError::InvalidInput); auto negativeQuantity = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); @@ -1343,9 +1330,9 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) invalidVisibility.auctionVisibility = 99; EXPECT_EQ(nostromo.createAuction(seller, invalidVisibility).errorCode, NOST::EAuctionError::InvalidVisibility); - auto invalidBatchBundle = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - invalidBatchBundle.auctionLotItems = ContractTestingNOST::makeTwoAssetLot(assetA, 2, assetB, 3); - EXPECT_EQ(nostromo.createAuction(seller, invalidBatchBundle).errorCode, NOST::EAuctionError::InvalidInput); + auto partiallyEmptyLot = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); + partiallyEmptyLot.auctionLotItems = ContractTestingNOST::makeSingleLot(Asset{}, 1); + EXPECT_EQ(nostromo.createAuction(seller, partiallyEmptyLot).errorCode, NOST::EAuctionError::InvalidInput); auto invalidBatchBuyNow = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); invalidBatchBuyNow.buyNowPrice = 100; @@ -1376,8 +1363,25 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) auto privateWithBothGates = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); privateWithBothGates.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); privateWithBothGates.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({id(99, 1, 1, 1)}); - privateWithBothGates.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({Asset{altIssuer, assetNameFromString("GATINV")}}); + privateWithBothGates.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAsset, 1}}); EXPECT_EQ(nostromo.createAuction(seller, privateWithBothGates, NOST_DEFAULT_PRIVATE_AUCTION_FEE).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(nostromo.createAuction(seller, zeroAccessQuantity, NOST_DEFAULT_PRIVATE_AUCTION_FEE).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(nostromo.createAuction(seller, negativeAccessQuantity, NOST_DEFAULT_PRIVATE_AUCTION_FEE).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(nostromo.createAuction(seller, partiallyEmptyAccessAsset, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, + NOST::EAuctionError::InvalidInput); } TEST(ContractNostromoAuction, CreateAuctionRejectsInsufficientFundsInsufficientAssetBalanceAndPauseAuction) @@ -1706,26 +1710,38 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) { ContractTestingNOST nostromo; const id seller(153, 154, 155, 156); - const id allowed(157, 158, 159, 160); - const id denied(161, 162, 163, 164); + 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 uint64 accessAssetName = assetNameFromString("PRIACG"); const Asset saleAsset{seller, saleAssetName}; - const Asset accessAsset{allowed, accessAssetName}; + const Asset accessAssetA{gateIssuerA, assetNameFromString("PRIAGA")}; + const Asset accessAssetB{gateIssuerB, assetNameFromString("PRIAGB")}; EXPECT_EQ(nostromo.issueAsset(seller, saleAssetName, 3), 3); - EXPECT_EQ(nostromo.issueAsset(allowed, accessAssetName, 1), 1); + 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({accessAsset}); + 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(denied, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::PrivateAuctionAccessDenied); - EXPECT_EQ(nostromo.placeBid(allowed, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.placeBid(belowThresholdBidder, createOutput.auctionIndex, 1, 12, 12).errorCode, + NOST::EAuctionError::PrivateAuctionAccessDenied); + EXPECT_EQ(nostromo.placeBid(exactThresholdBidder, createOutput.auctionIndex, 1, 12, 12).errorCode, + NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.placeBid(alternateAssetBidder, createOutput.auctionIndex, 1, 13, 13).errorCode, + NOST::EAuctionError::Success); } } @@ -1734,17 +1750,13 @@ TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) ContractTestingNOST nostromo; const id seller(171, 172, 173, 174); const id bidder(175, 176, 177, 178); - const uint64 assetNameA = assetNameFromString("BUYNWA"); - const uint64 assetNameB = assetNameFromString("BUYNWB"); - const Asset assetA{seller, assetNameA}; - const Asset assetB{seller, assetNameB}; + const uint64 assetName = assetNameFromString("BUYNWA"); + const Asset asset{seller, assetName}; - EXPECT_EQ(nostromo.issueAsset(seller, assetNameA, 2), 2); - EXPECT_EQ(nostromo.issueAsset(seller, assetNameB, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetA, 2), 2); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, assetB, 1), 1); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); - auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeTwoAssetLot(assetA, 2, assetB, 1), 100, 150, 10, 180); + auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 3), 100, 150, 10, 180); NOST::AuctionRevenueBreakdown expectedRevenue{}; nostromo.calculateAuctionRevenueBreakdown(180ULL, expectedRevenue); const auto createOutput = nostromo.createAuction(seller, input); @@ -1762,8 +1774,7 @@ TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) EXPECT_EQ(participant.participantData.allocatedQuantity, 1ULL); EXPECT_EQ(participant.participantData.escrowedAmount, 0ULL); EXPECT_EQ(participant.participantData.isWinningBid, 1u); - EXPECT_EQ(nostromo.managedShares(assetA, bidder), 2); - EXPECT_EQ(nostromo.managedShares(assetB, bidder), 1); + EXPECT_EQ(nostromo.managedShares(asset, bidder), 3); EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, expectedRevenue.sellerPayout); } From 9966a7543e220d3c04744a881507e9a56d62f7ed Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 10 Jun 2026 22:28:00 +0300 Subject: [PATCH 44/59] Removes magic numbers --- src/contracts/Nostromo.h | 255 ++++++++++++++++++++++++--------------- 1 file changed, 160 insertions(+), 95 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index bb60161ec..1ca2e8fd3 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -39,7 +39,20 @@ constexpr uint64 NOST_SECONDS_PER_DAY = 86400ULL; constexpr uint64 NOST_AUCTION_SELLER_DECISION_WINDOW_SECONDS = 604800ULL; constexpr uint64 NOST_AUCTION_PRE_EPOCH_PAUSE_SECONDS = 1800ULL; constexpr uint32 NOST_AUCTION_POST_BEGIN_EPOCH_PAUSE_TICKS = 500U; -constexpr uint32 NOST_DEFAULT_INIT_TIME = 22 << 9 | 4 << 5 | 13; +constexpr uint64 NOST_BASIS_POINTS_SCALE = 10000ULL; +constexpr uint64 NOST_MICROSECONDS_PER_SECOND = 1000000ULL; +constexpr uint16 NOST_REINITIALIZATION_EPOCH = 220U; +constexpr uint64 NOST_STANDARD_AUCTION_LOT_COUNT = 1ULL; +constexpr uint8 NOST_DEFAULT_INIT_YEAR = 22U; +constexpr uint8 NOST_DEFAULT_INIT_MONTH = 4U; +constexpr uint8 NOST_DEFAULT_INIT_DAY = 13U; +constexpr uint8 NOST_DATE_STAMP_YEAR_SHIFT = 9U; +constexpr uint8 NOST_DATE_STAMP_MONTH_SHIFT = 5U; +constexpr uint8 NOST_PRE_EPOCH_PAUSE_DAY_OF_WEEK = 0U; +constexpr uint8 NOST_PRE_EPOCH_PAUSE_HOUR = 11U; +constexpr uint8 NOST_PRE_EPOCH_PAUSE_MINUTE = 30U; +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; constexpr uint8 NOST_ROUTE_ALL_FEES_TO_DEVELOPMENT = 1; struct NOST2 @@ -48,6 +61,18 @@ struct NOST2 struct NOST : public ContractBase { + enum class EProcedureId : uint8 + { + CreateAuction = 1, + PlaceBid = 2, + CancelAuction = 3, + TransferShareManagementRights = 4, + ResolvePendingStandardAuction = 5, + SetAuctionFees = 6, + SetAuctionFeesByManagement = 7, + SetManagement = 8 + }; + enum class EAuctionType : uint8 { None, @@ -1601,14 +1626,14 @@ struct NOST : public ContractBase REGISTER_USER_FUNCTIONS_AND_PROCEDURES() { - REGISTER_USER_PROCEDURE(CreateAuction, 1); - REGISTER_USER_PROCEDURE(PlaceBid, 2); - REGISTER_USER_PROCEDURE(CancelAuction, 3); - REGISTER_USER_PROCEDURE(TransferShareManagementRights, 4); - REGISTER_USER_PROCEDURE(ResolvePendingStandardAuction, 5); - REGISTER_USER_PROCEDURE(SetAuctionFees, 6); - REGISTER_USER_PROCEDURE(SetAuctionFeesByManagement, 7); - REGISTER_USER_PROCEDURE(SetManagement, 8); + 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_FUNCTION(GetAuctionByIndex, 1); REGISTER_USER_FUNCTION(GetAuctionParticipant, 2); @@ -1665,7 +1690,7 @@ struct NOST : public ContractBase BEGIN_EPOCH_WITH_LOCALS() { // TODO: Change to valid epoch - if (qpi.epoch() == 220) + if (qpi.epoch() == NOST_REINITIALIZATION_EPOCH) { // Initialize state.mut().privateAuctionFee = NOST_DEFAULT_PRIVATE_AUCTION_FEE; @@ -1833,13 +1858,14 @@ struct NOST : public ContractBase return; } - if (qpi.dayOfWeek(qpi.year(), qpi.month(), qpi.day()) == 0 && qpi.hour() == 11 && qpi.minute() >= 30) + 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(11, 30, 0, 0, 0); - output.pauseEndsAt = locals.currentDate; - output.pauseEndsAt.setTime(12, 0, 0, 0, 0); + 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); } } @@ -2776,7 +2802,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::AuctionPaused; - setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); logProcedureResult(locals.log); return; } @@ -2788,7 +2815,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::InvalidAuctionType; - setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); logProcedureResult(locals.log); return; } @@ -2800,7 +2828,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::InvalidVisibility; - setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); logProcedureResult(locals.log); return; } @@ -2812,7 +2841,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::StorageFull; - setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); logProcedureResult(locals.log); return; } @@ -2824,7 +2854,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::AuctionIndexExhausted; - setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); logProcedureResult(locals.log); return; } @@ -2838,7 +2869,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); logProcedureResult(locals.log); return; } @@ -2853,7 +2885,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); logProcedureResult(locals.log); return; } @@ -2871,7 +2904,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); logProcedureResult(locals.log); return; } @@ -2886,7 +2920,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); logProcedureResult(locals.log); return; } @@ -2897,7 +2932,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::InvalidAuctionType; - setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); logProcedureResult(locals.log); return; } @@ -2916,7 +2952,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); logProcedureResult(locals.log); return; } @@ -2929,7 +2966,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::InsufficientFunds; - setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); logProcedureResult(locals.log); return; } @@ -2943,7 +2981,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::InsufficientAssetBalance; - setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); logProcedureResult(locals.log); return; } @@ -2957,7 +2996,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::InsufficientAssetBalance; - setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); logProcedureResult(locals.log); return; } @@ -3007,7 +3047,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::StorageFull; - setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, + qpi.invocationReward()); logProcedureResult(locals.log); return; } @@ -3023,7 +3064,7 @@ struct NOST : public ContractBase output.auctionIndex = locals.auction.core.auctionIndex; state.mut().totalAuctionsCreated = sadd(state.get().totalAuctionsCreated, 1ULL); output.errorCode = EAuctionError::Success; - setProcedureLogInput(locals.log, qpi.invocator(), 1, output.errorCode, output.auctionIndex, qpi.invocationReward()); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CreateAuction, output.errorCode, output.auctionIndex, qpi.invocationReward()); logProcedureResult(locals.log); } @@ -3044,7 +3085,7 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::AuctionPaused; - setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); logProcedureResult(locals.log); return; } @@ -3056,7 +3097,7 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::AuctionNotFound; - setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); logProcedureResult(locals.log); return; } @@ -3068,7 +3109,7 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::AuctionClosed; - setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); logProcedureResult(locals.log); return; } @@ -3080,7 +3121,7 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::Forbidden; - setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); logProcedureResult(locals.log); return; } @@ -3094,7 +3135,7 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::AuctionClosed; - setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); logProcedureResult(locals.log); return; } @@ -3119,7 +3160,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::PrivateAuctionAccessDenied; - setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, + output.escrowedAmount); logProcedureResult(locals.log); return; } @@ -3141,7 +3183,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = locals.processBatchBidOutput.errorCode; - setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, + output.escrowedAmount); logProcedureResult(locals.log); return; } @@ -3161,7 +3204,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = locals.processStandardBidOutput.errorCode; - setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, + output.escrowedAmount); logProcedureResult(locals.log); return; } @@ -3174,12 +3218,13 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::InvalidAuctionType; - setProcedureLogInput(locals.log, qpi.invocator(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + 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(), 2, output.errorCode, input.auctionIndex, output.escrowedAmount); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::PlaceBid, output.errorCode, input.auctionIndex, output.escrowedAmount); logProcedureResult(locals.log); } @@ -3202,7 +3247,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::AuctionNotFound; - setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); logProcedureResult(locals.log); return; } @@ -3214,7 +3260,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::AuctionClosed; - setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); logProcedureResult(locals.log); return; } @@ -3226,7 +3273,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::Forbidden; - setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); logProcedureResult(locals.log); return; } @@ -3238,7 +3286,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::Forbidden; - setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); logProcedureResult(locals.log); return; } @@ -3248,7 +3297,8 @@ struct NOST : public ContractBase { locals.cancellationBaseAmount = smul(locals.auction.core.salePrice, locals.auction.core.quantityForSale); } - output.cancellationFee = div(smul(locals.cancellationBaseAmount, state.get().auctionCancellationFeeBasisPoints), 10000ULL); + output.cancellationFee = + div(smul(locals.cancellationBaseAmount, state.get().auctionCancellationFeeBasisPoints), NOST_BASIS_POINTS_SCALE); if (static_cast(qpi.invocationReward()) < output.cancellationFee) { @@ -3257,7 +3307,8 @@ struct NOST : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } output.errorCode = EAuctionError::InsufficientFunds; - setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); logProcedureResult(locals.log); return; } @@ -3300,7 +3351,7 @@ struct NOST : public ContractBase } output.errorCode = EAuctionError::Success; - setProcedureLogInput(locals.log, qpi.invocator(), 3, output.errorCode, input.auctionIndex, output.cancellationFee); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, output.cancellationFee); logProcedureResult(locals.log); } @@ -3322,7 +3373,8 @@ struct NOST : public ContractBase if (locals.isAuctionInteractionPausedOutput.isPaused) { output.errorCode = EAuctionError::AuctionPaused; - setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); logProcedureResult(locals.log); return; } @@ -3330,7 +3382,8 @@ struct NOST : public ContractBase if (input.acceptSale > 1) { output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); logProcedureResult(locals.log); return; } @@ -3338,7 +3391,8 @@ struct NOST : public ContractBase if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { output.errorCode = EAuctionError::AuctionNotFound; - setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); logProcedureResult(locals.log); return; } @@ -3346,7 +3400,8 @@ struct NOST : public ContractBase if (locals.auction.core.seller != qpi.invocator()) { output.errorCode = EAuctionError::Forbidden; - setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); logProcedureResult(locals.log); return; } @@ -3354,7 +3409,8 @@ struct NOST : public ContractBase if (locals.auction.core.type != EAuctionType::Standard || locals.auction.core.status != EAuctionStatus::PendingSellerDecision) { output.errorCode = EAuctionError::AuctionClosed; - setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); logProcedureResult(locals.log); return; } @@ -3367,7 +3423,8 @@ struct NOST : public ContractBase CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); output.errorCode = EAuctionError::AuctionClosed; - setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); logProcedureResult(locals.log); return; } @@ -3387,7 +3444,8 @@ struct NOST : public ContractBase output.refundedAmount = locals.rejectStandardAuctionOutput.refundedAmount; output.errorCode = locals.rejectStandardAuctionOutput.success ? EAuctionError::Success : EAuctionError::AuctionClosed; } - setProcedureLogInput(locals.log, qpi.invocator(), 5, output.errorCode, input.auctionIndex, output.refundedAmount); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::ResolvePendingStandardAuction, output.errorCode, input.auctionIndex, + output.refundedAmount); logProcedureResult(locals.log); } @@ -3406,7 +3464,7 @@ struct NOST : public ContractBase if (qpi.invocator() != state.get().takeoverCoordinator) { output.errorCode = EAuctionError::Forbidden; - setProcedureLogInput(locals.log, qpi.invocator(), 6, output.errorCode, 0, 0); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFees, output.errorCode, 0, 0); logProcedureResult(locals.log); return; } @@ -3417,7 +3475,7 @@ struct NOST : public ContractBase input.shareholderFeeBasisPointsTier2, input.shareholderFeeBasisPointsTier3, input.shareholderFeeBasisPointsTier4)) { output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), 6, output.errorCode, 0, 0); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFees, output.errorCode, 0, 0); logProcedureResult(locals.log); return; } @@ -3433,7 +3491,7 @@ struct NOST : public ContractBase state.mut().shareholderFeeBasisPointsTier3 = input.shareholderFeeBasisPointsTier3; state.mut().shareholderFeeBasisPointsTier4 = input.shareholderFeeBasisPointsTier4; output.errorCode = EAuctionError::Success; - setProcedureLogInput(locals.log, qpi.invocator(), 6, output.errorCode, 0, 0); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFees, output.errorCode, 0, 0); logProcedureResult(locals.log); } @@ -3453,7 +3511,7 @@ struct NOST : public ContractBase if (qpi.invocator() != state.get().management) { output.errorCode = EAuctionError::Forbidden; - setProcedureLogInput(locals.log, qpi.invocator(), 7, output.errorCode, 0, 0); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFeesByManagement, output.errorCode, 0, 0); logProcedureResult(locals.log); return; } @@ -3464,7 +3522,7 @@ struct NOST : public ContractBase input.shareholderFeeBasisPointsTier2, input.shareholderFeeBasisPointsTier3, input.shareholderFeeBasisPointsTier4)) { output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), 7, output.errorCode, 0, 0); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFeesByManagement, output.errorCode, 0, 0); logProcedureResult(locals.log); return; } @@ -3478,7 +3536,7 @@ struct NOST : public ContractBase state.mut().shareholderFeeBasisPointsTier3 = input.shareholderFeeBasisPointsTier3; state.mut().shareholderFeeBasisPointsTier4 = input.shareholderFeeBasisPointsTier4; output.errorCode = EAuctionError::Success; - setProcedureLogInput(locals.log, qpi.invocator(), 7, output.errorCode, 0, 0); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetAuctionFeesByManagement, output.errorCode, 0, 0); logProcedureResult(locals.log); } @@ -3498,7 +3556,7 @@ struct NOST : public ContractBase if (qpi.invocator() != state.get().takeoverCoordinator) { output.errorCode = EAuctionError::Forbidden; - setProcedureLogInput(locals.log, qpi.invocator(), 8, output.errorCode, 0, 0); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetManagement, output.errorCode, 0, 0); logProcedureResult(locals.log); return; } @@ -3506,14 +3564,14 @@ struct NOST : public ContractBase if (isZero(input.management)) { output.errorCode = EAuctionError::InvalidInput; - setProcedureLogInput(locals.log, qpi.invocator(), 8, output.errorCode, 0, 0); + 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(), 8, output.errorCode, 0, 0); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetManagement, output.errorCode, 0, 0); logProcedureResult(locals.log); } @@ -3888,7 +3946,8 @@ struct NOST : public ContractBase { qpi.transfer(qpi.invocator(), locals.refundAmount); } - setProcedureLogInput(locals.log, qpi.invocator(), 4, output.errorCode, 0, output.transferredNumberOfShares); + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::TransferShareManagementRights, output.errorCode, 0, + output.transferredNumberOfShares); logProcedureResult(locals.log); } @@ -3906,11 +3965,11 @@ struct NOST : public ContractBase } } - static void setProcedureLogInput(NostromoProcedureLog& log, const id& actor, uint8 procedure, EAuctionError errorCode, uint64 auctionIndex, + static void setProcedureLogInput(NostromoProcedureLog& log, const id& actor, EProcedureId procedure, EAuctionError errorCode, uint64 auctionIndex, sint64 amount) { log.contractIndex = SELF_INDEX; - log.procedure = procedure; + log.procedure = static_cast(procedure); log.errorCode = static_cast(errorCode); log.auctionIndex = auctionIndex; log.actor = actor; @@ -3972,12 +4031,13 @@ struct NOST : public ContractBase { return a > b ? a : b; } + static bool resolveBatchAuctionCreateParams(uint64 lotItemCount, uint64 totalEscrowQuantity, uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPrice) { quantityForSale = 0; resolvedMinimumPurchaseQuantity = 0; - if (lotItemCount != 1 || totalEscrowQuantity == 0 || buyNowPrice != 0) + if (lotItemCount != NOST_AUCTION_LOT_ITEM_NUM || totalEscrowQuantity == 0 || buyNowPrice != 0) { return false; } @@ -3991,7 +4051,7 @@ struct NOST : public ContractBase { quantityForSale = 0; resolvedMinimumPurchaseQuantity = 0; - if (minimumPurchaseQuantity > 1 || minimumBidIncrement == 0 || salePrice == 0) + if (minimumPurchaseQuantity > NOST_STANDARD_AUCTION_LOT_COUNT || minimumBidIncrement == 0 || salePrice == 0) { return false; } @@ -4007,8 +4067,8 @@ struct NOST : public ContractBase return false; } - quantityForSale = 1; - resolvedMinimumPurchaseQuantity = 1; + quantityForSale = NOST_STANDARD_AUCTION_LOT_COUNT; + resolvedMinimumPurchaseQuantity = NOST_STANDARD_AUCTION_LOT_COUNT; return true; } @@ -4017,24 +4077,25 @@ struct NOST : public ContractBase return visibility != EAuctionVisibility::Private || ((requiredAccessAssetCount > 0) != (allowedWalletCount > 0)); } - constexpr static bool isValidAuctionFeeConfiguration(sint64 privateAuctionFee, uint64 auctionCancellationFeeBasisPoints, uint64 managementFeeBasisPoints, - uint64 developmentFeeBasisPoints, uint64 takeoverCoordinatorFeeBasisPoints, - uint64 shareholderDividendBasisPoints, uint64 shareholderFeeBasisPointsTier1, - uint64 shareholderFeeBasisPointsTier2, uint64 shareholderFeeBasisPointsTier3, - uint64 shareholderFeeBasisPointsTier4) - { - return privateAuctionFee >= 0 && auctionCancellationFeeBasisPoints <= 10000ULL && managementFeeBasisPoints <= 10000ULL && - developmentFeeBasisPoints <= 10000ULL && takeoverCoordinatorFeeBasisPoints <= 10000ULL && shareholderDividendBasisPoints <= 10000ULL && - shareholderFeeBasisPointsTier1 <= 10000ULL && shareholderFeeBasisPointsTier2 <= 10000ULL && - shareholderFeeBasisPointsTier3 <= 10000ULL && shareholderFeeBasisPointsTier4 <= 10000ULL && + constexpr static bool isValidAuctionFeeConfiguration(sint64 privateAuctionFee, uint64 auctionCancellationFeeBasisPoints, + uint64 managementFeeBasisPoints, uint64 developmentFeeBasisPoints, + uint64 takeoverCoordinatorFeeBasisPoints, uint64 shareholderDividendBasisPoints, + uint64 shareholderFeeBasisPointsTier1, uint64 shareholderFeeBasisPointsTier2, + uint64 shareholderFeeBasisPointsTier3, uint64 shareholderFeeBasisPointsTier4) + { + return privateAuctionFee >= 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) <= - 10000ULL && + NOST_BASIS_POINTS_SCALE && (shareholderFeeBasisPointsTier2 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= - 10000ULL && + NOST_BASIS_POINTS_SCALE && (shareholderFeeBasisPointsTier3 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= - 10000ULL && + NOST_BASIS_POINTS_SCALE && (shareholderFeeBasisPointsTier4 + managementFeeBasisPoints + developmentFeeBasisPoints + takeoverCoordinatorFeeBasisPoints) <= - 10000ULL; + NOST_BASIS_POINTS_SCALE; } static uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount, const StateData& state) @@ -4068,11 +4129,12 @@ struct NOST : public ContractBase { output.sellerPayout = grossAmount; output.shareholderFeeBasisPoints = getAuctionShareholderFeeBasisPoints(grossAmount, state); - output.shareholderFeeAmount = div(smul(grossAmount, output.shareholderFeeBasisPoints), 10000ULL); - output.shareholderDividendAmount = div(smul(output.shareholderFeeAmount, state.get().shareholderDividendBasisPoints), 10000ULL); - output.managementFeeAmount = div(smul(grossAmount, state.get().managementFeeBasisPoints), 10000ULL); - output.developmentFeeAmount = div(smul(grossAmount, state.get().developmentFeeBasisPoints), 10000ULL); - output.takeoverCoordinatorBaseAmount = div(smul(grossAmount, state.get().takeoverCoordinatorFeeBasisPoints), 10000ULL); + output.shareholderFeeAmount = div(smul(grossAmount, output.shareholderFeeBasisPoints), NOST_BASIS_POINTS_SCALE); + output.shareholderDividendAmount = + div(smul(output.shareholderFeeAmount, state.get().shareholderDividendBasisPoints), NOST_BASIS_POINTS_SCALE); + output.managementFeeAmount = div(smul(grossAmount, state.get().managementFeeBasisPoints), NOST_BASIS_POINTS_SCALE); + output.developmentFeeAmount = div(smul(grossAmount, state.get().developmentFeeBasisPoints), NOST_BASIS_POINTS_SCALE); + output.takeoverCoordinatorBaseAmount = div(smul(grossAmount, state.get().takeoverCoordinatorFeeBasisPoints), NOST_BASIS_POINTS_SCALE); output.takeoverCoordinatorFeeAmount = output.takeoverCoordinatorBaseAmount + (output.shareholderFeeAmount - output.shareholderDividendAmount); output.sellerPayout = grossAmount - output.shareholderFeeAmount - output.managementFeeAmount - output.developmentFeeAmount - output.takeoverCoordinatorBaseAmount; @@ -4084,10 +4146,10 @@ struct NOST : public ContractBase */ static void calculateAuctionServiceFeeBreakdown(uint64 feeAmount, AuctionServiceFeeBreakdown& output) { - output.shareholderDividendAmount = div(smul(feeAmount, NOST_AUCTION_SERVICE_FEE_SHAREHOLDER_BP), 10000ULL); - output.managementFeeAmount = div(smul(feeAmount, NOST_AUCTION_SERVICE_FEE_MANAGEMENT_BP), 10000ULL); - output.developmentFeeAmount = div(smul(feeAmount, NOST_AUCTION_SERVICE_FEE_DEVELOPMENT_BP), 10000ULL); - output.takeoverCoordinatorFeeAmount = div(smul(feeAmount, NOST_AUCTION_SERVICE_FEE_TAKEOVER_COORDINATOR_BP), 10000ULL); + output.shareholderDividendAmount = div(smul(feeAmount, NOST_AUCTION_SERVICE_FEE_SHAREHOLDER_BP), NOST_BASIS_POINTS_SCALE); + output.managementFeeAmount = div(smul(feeAmount, NOST_AUCTION_SERVICE_FEE_MANAGEMENT_BP), NOST_BASIS_POINTS_SCALE); + output.developmentFeeAmount = div(smul(feeAmount, NOST_AUCTION_SERVICE_FEE_DEVELOPMENT_BP), NOST_BASIS_POINTS_SCALE); + output.takeoverCoordinatorFeeAmount = div(smul(feeAmount, NOST_AUCTION_SERVICE_FEE_TAKEOVER_COORDINATOR_BP), NOST_BASIS_POINTS_SCALE); // Shareholders receive the rounding remainder so the entire collected fee is distributed on-chain. output.shareholderDividendAmount = output.shareholderDividendAmount + (feeAmount - output.shareholderDividendAmount - output.managementFeeAmount - @@ -4123,7 +4185,10 @@ struct NOST : public ContractBase state.mut().closedAuctionHistoryCounter = sadd(state.get().closedAuctionHistoryCounter, 1ULL); } - static void makeDateStamp(uint8 year, uint8 month, uint8 day, uint32& res) { res = static_cast(year << 9 | month << 5 | day); } + 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); + } static void accumulatePauseWindow(uint8& hasPauseWindow, DateAndTime& pauseStartedAt, DateAndTime& pauseEndsAt, const DateAndTime& candidatePauseStartedAt, const DateAndTime& candidatePauseEndsAt) @@ -4178,6 +4243,6 @@ struct NOST : public ContractBase res = 0; return; } - res = div(a.durationMicrosec(b), 1000000ULL); + res = div(a.durationMicrosec(b), NOST_MICROSECONDS_PER_SECOND); } }; From 1a552232500225cf41cfd152c12fa87dd2046fe1 Mon Sep 17 00:00:00 2001 From: N-010 Date: Sun, 14 Jun 2026 19:58:34 +0300 Subject: [PATCH 45/59] Batch 1 lot, Standard 4 lots per auction --- src/contracts/Nostromo.h | 9 ++-- test/contract_nostromo.cpp | 91 +++++++++++++++++++++++++++----------- 2 files changed, 71 insertions(+), 29 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 1ca2e8fd3..b14287c6d 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -13,7 +13,8 @@ constexpr uint64 NOST_AUCTION_HISTORY_NUM = 1024; constexpr uint64 NOST_AUCTION_METADATA_CID_LENGTH = 64; constexpr uint64 NOST_AUCTION_PARTICIPANT_NUM = 4096; constexpr uint64 NOST_AUCTION_GETTER_PAGE_SIZE = 64; -constexpr uint64 NOST_AUCTION_LOT_ITEM_NUM = 1; +constexpr uint64 NOST_BATCH_AUCTION_LOT_ITEM_NUM = 1; +constexpr uint64 NOST_AUCTION_LOT_ITEM_NUM = 4; constexpr uint64 NOST_AUCTION_ALLOWED_WALLET_NUM = 16; constexpr uint64 NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM = 4; constexpr uint32 NOST_AUCTION_MAX_DURATION_DAYS = 30; @@ -170,7 +171,7 @@ struct NOST : public ContractBase */ struct AuctionCore { - /** @brief Single asset and quantity offered by the auction. */ + /** @brief Assets and quantities offered by the auction. */ Array auctionLotItems; /** @brief Lowercase base32 CIDv1 stored in Pinata for the auction name and description metadata. */ @@ -361,7 +362,7 @@ struct NOST : public ContractBase /** @brief Lowercase base32 CIDv1 stored in Pinata for the auction name and description metadata. */ Array metadataIpfsCid; - /** @brief Single asset and quantity offered by the auction. */ + /** @brief Assets and quantities offered by the auction. */ Array auctionLotItems; /** @brief Asset and minimum-quantity alternatives used by private asset-based access. */ @@ -4037,7 +4038,7 @@ struct NOST : public ContractBase { quantityForSale = 0; resolvedMinimumPurchaseQuantity = 0; - if (lotItemCount != NOST_AUCTION_LOT_ITEM_NUM || totalEscrowQuantity == 0 || buyNowPrice != 0) + if (lotItemCount != NOST_BATCH_AUCTION_LOT_ITEM_NUM || totalEscrowQuantity == 0 || buyNowPrice != 0) { return false; } diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index c11114c90..9ff436f0c 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -516,6 +516,17 @@ class ContractTestingNOST : protected ContractTesting 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{}; @@ -964,31 +975,66 @@ TEST(ContractNostromoAuction, CreateStandardSingleAssetAuctionEscrowsLotAuction) EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 5); } -TEST(ContractNostromoAuction, CreateStandardAuctionSupportsSingleLotEntryAuction) +TEST(ContractNostromoAuction, CreateStandardAuctionSupportsFourLotEntriesAuction) { ContractTestingNOST nostromo; const id seller(25, 26, 27, 28); const id bidder(29, 30, 31, 32); - const uint64 assetName = assetNameFromString("MAXLOT"); - const Asset asset{seller, assetName}; + const Asset assets[] = { + {seller, assetNameFromString("MAXLOA")}, + {seller, assetNameFromString("MAXLOB")}, + {seller, assetNameFromString("MAXLOC")}, + {seller, assetNameFromString("MAXLOD")}, + }; - EXPECT_EQ(NOST_AUCTION_LOT_ITEM_NUM, 1ULL); - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); + 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::makeSingleLot(asset, 3), 100, 100, 1)); + seller, ContractTestingNOST::makeStandardAuctionInput( + ContractTestingNOST::makeLot({{assets[0], 3}, {assets[1], 3}, {assets[2], 3}, {assets[3], 3}}), 100, 100, 1)); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.managedShares(asset, seller), 0); - EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 3); + 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, 100, 100).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(nostromo.managedShares(asset, bidder), 3); - EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 0); + 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) @@ -1091,8 +1137,8 @@ TEST(ContractNostromoAuction, GetAuctionViewExposesAccessListsAndFoundFlagAuctio 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}}); + 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); @@ -1161,8 +1207,7 @@ TEST(ContractNostromoAuction, GetAuctionViewDeduplicatesPrivateAccessInputsAucti const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; EXPECT_EQ(auction.requiredAccessAssetCount, 1U); - EXPECT_TRUE(containsAccessAsset(auction.requiredAccessAssets, auction.requiredAccessAssetCount, - NOST::AuctionAssetEntry{accessAsset, 5})); + EXPECT_TRUE(containsAccessAsset(auction.requiredAccessAssets, auction.requiredAccessAssetCount, NOST::AuctionAssetEntry{accessAsset, 5})); } } @@ -1211,8 +1256,7 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacit { 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}); + 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); @@ -1374,8 +1418,7 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) auto negativeAccessQuantity = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); negativeAccessQuantity.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); negativeAccessQuantity.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAsset, -1}}); - EXPECT_EQ(nostromo.createAuction(seller, negativeAccessQuantity, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, - NOST::EAuctionError::InvalidInput); + EXPECT_EQ(nostromo.createAuction(seller, negativeAccessQuantity, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, NOST::EAuctionError::InvalidInput); auto partiallyEmptyAccessAsset = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); partiallyEmptyAccessAsset.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); @@ -1730,18 +1773,16 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) 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}}); + 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.placeBid(exactThresholdBidder, createOutput.auctionIndex, 1, 12, 12).errorCode, - NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.placeBid(alternateAssetBidder, createOutput.auctionIndex, 1, 13, 13).errorCode, - NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.placeBid(exactThresholdBidder, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.placeBid(alternateAssetBidder, createOutput.auctionIndex, 1, 13, 13).errorCode, NOST::EAuctionError::Success); } } From 74d1a8ccc4951aa23f9639f9ad6b840499249e4f Mon Sep 17 00:00:00 2001 From: N-010 Date: Sun, 14 Jun 2026 20:16:04 +0300 Subject: [PATCH 46/59] Adds comments --- src/contracts/Nostromo.h | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index b14287c6d..30bf675d1 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -8,52 +8,98 @@ namespace QPI } } // namespace QPI +// Maximum number of active auction records stored by the contract, in auctions. constexpr uint64 NOST_AUCTION_NUM = 2048; +// Number of closed auction indices 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 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; +// 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 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 private-creation and cancellation service fees, in basis points. constexpr uint64 NOST_AUCTION_SERVICE_FEE_SHAREHOLDER_BP = 7270ULL; +// Management allocation of private-creation and cancellation service fees, in basis points. constexpr uint64 NOST_AUCTION_SERVICE_FEE_MANAGEMENT_BP = 910ULL; +// Development allocation of private-creation and cancellation service fees, in basis points. constexpr uint64 NOST_AUCTION_SERVICE_FEE_DEVELOPMENT_BP = 910ULL; +// Takeover coordinator allocation of private-creation 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; +// 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; struct NOST2 From dd046d94689f2bab07067354036860bfaa46662e Mon Sep 17 00:00:00 2001 From: N-010 Date: Fri, 19 Jun 2026 22:04:08 +0300 Subject: [PATCH 47/59] Minimum Purchase Quantity in Batch Auction --- src/contracts/Nostromo.h | 47 ++++++++----- test/contract_nostromo.cpp | 136 +++++++++++++++++++++++++++++++++++-- 2 files changed, 159 insertions(+), 24 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 30bf675d1..21b03cf06 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -247,7 +247,7 @@ struct NOST : public ContractBase /** @brief Quantity already assigned to winning bids after settlement. */ uint64 allocatedQuantity; - /** @brief Reserved for standard auction validation; batch auctions do not enforce a minimum bid quantity. */ + /** @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. */ @@ -417,7 +417,7 @@ struct NOST : public ContractBase /** @brief Wallet list used when the private auction restricts participation to predefined wallets. */ Array allowedBidderWallets; - /** @brief Reserved for standard auction validation; batch auctions ignore this value. */ + /** @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. */ @@ -459,7 +459,7 @@ struct NOST : public ContractBase /** @brief Monotonic index of the target auction. */ uint64 auctionIndex; - /** @brief Requested quantity for a batch auction; ignored for a standard auction because the whole lot is sold as one unit. */ + /** @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. */ @@ -1116,7 +1116,10 @@ struct NOST : public ContractBase /** @brief Internal output returned after batch auction finalization. */ struct FinalizeBatchAuction_output { - /** @brief Flag indicating whether batch settlement finished successfully. */ + /** + * @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; }; @@ -2273,8 +2276,9 @@ struct NOST : public ContractBase return; } - if (input.effectiveQuantity == 0 || input.bidAmount == 0) + if (input.effectiveQuantity < locals.auction.core.minimumPurchaseQuantity || input.bidAmount == 0) { + output.refundedAmount = static_cast(qpi.invocationReward()); output.errorCode = EAuctionError::InvalidInput; return; } @@ -2576,9 +2580,9 @@ struct NOST : public ContractBase return; } - // Repeatedly pick the best remaining bid, allocate available quantity, and collect the winning payment. + // Once supply falls below the minimum, no valid allocation remains; the common refund path returns all residual escrow and assets. locals.remainingQuantity = locals.auction.core.quantityForSale; - while (locals.remainingQuantity > 0) + while (locals.remainingQuantity >= locals.auction.core.minimumPurchaseQuantity) { locals.bestParticipantFound = 0; locals.participantIndex = state.get().participants.nextElementIndex(NULL_INDEX); @@ -2834,6 +2838,7 @@ struct NOST : public ContractBase /** * @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 Private auctions require the configured private auction fee, which is distributed between shareholders and the configured fee recipients, * and must use exactly one access mode. */ @@ -2944,7 +2949,8 @@ struct NOST : public ContractBase case EAuctionType::Batch: if (!resolveBatchAuctionCreateParams(locals.analyzeAuctionLotOutput.lotItemCount, locals.analyzeAuctionLotOutput.totalEscrowQuantity, - locals.resolvedQuantityForSale, locals.resolvedMinimumPurchaseQuantity, input.buyNowPrice)) + input.minimumPurchaseQuantity, locals.resolvedQuantityForSale, + locals.resolvedMinimumPurchaseQuantity, input.buyNowPrice)) { if (qpi.invocationReward() > 0) { @@ -2958,7 +2964,7 @@ struct NOST : public ContractBase } break; case EAuctionType::Standard: - if (!resolveStandardAuctionCreateParams(input.minimumPurchaseQuantity, input.minimumBidIncrement, locals.resolvedQuantityForSale, + if (!resolveStandardAuctionCreateParams(input.minimumBidIncrement, locals.resolvedQuantityForSale, locals.resolvedMinimumPurchaseQuantity, input.buyNowPrice, input.initialPrice, input.salePrice)) { @@ -3117,7 +3123,10 @@ struct NOST : public ContractBase /** * @brief Places a bid in an active auction. - * @note Batch auctions interpret `bidAmount` as price per asset and `quantity` as the requested amount. + * @note Batch auctions interpret `bidAmount` as price per asset and reject requested `quantity` below `minimumPurchaseQuantity` with a full + * refund. + * @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) @@ -3229,6 +3238,7 @@ struct NOST : public ContractBase { 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); @@ -4079,26 +4089,27 @@ struct NOST : public ContractBase return a > b ? a : b; } - static bool resolveBatchAuctionCreateParams(uint64 lotItemCount, uint64 totalEscrowQuantity, uint64& quantityForSale, - uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPrice) + static bool resolveBatchAuctionCreateParams(uint64 lotItemCount, uint64 totalEscrowQuantity, uint64 minimumPurchaseQuantity, + uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPrice) { quantityForSale = 0; resolvedMinimumPurchaseQuantity = 0; - if (lotItemCount != NOST_BATCH_AUCTION_LOT_ITEM_NUM || totalEscrowQuantity == 0 || buyNowPrice != 0) + if (lotItemCount != NOST_BATCH_AUCTION_LOT_ITEM_NUM || totalEscrowQuantity == 0 || minimumPurchaseQuantity == 0 || + minimumPurchaseQuantity > totalEscrowQuantity || buyNowPrice != 0) { return false; } quantityForSale = totalEscrowQuantity; - resolvedMinimumPurchaseQuantity = 0; + resolvedMinimumPurchaseQuantity = minimumPurchaseQuantity; return true; } - static bool resolveStandardAuctionCreateParams(uint64 minimumPurchaseQuantity, uint64 minimumBidIncrement, uint64& quantityForSale, - uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPrice, uint64 initialPrice, uint64 salePrice) + static bool resolveStandardAuctionCreateParams(uint64 minimumBidIncrement, uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity, + uint64 buyNowPrice, uint64 initialPrice, uint64 salePrice) { quantityForSale = 0; resolvedMinimumPurchaseQuantity = 0; - if (minimumPurchaseQuantity > NOST_STANDARD_AUCTION_LOT_COUNT || minimumBidIncrement == 0 || salePrice == 0) + if (minimumBidIncrement == 0 || salePrice == 0) { return false; } @@ -4115,7 +4126,7 @@ struct NOST : public ContractBase } quantityForSale = NOST_STANDARD_AUCTION_LOT_COUNT; - resolvedMinimumPurchaseQuantity = NOST_STANDARD_AUCTION_LOT_COUNT; + resolvedMinimumPurchaseQuantity = 0; return true; } diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index 9ff436f0c..dc8595066 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -555,6 +555,7 @@ class ContractTestingNOST : protected ContractTesting 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); @@ -934,7 +935,7 @@ TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) 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, 0ULL); + 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); @@ -959,13 +960,14 @@ TEST(ContractNostromoAuction, CreateStandardSingleAssetAuctionEscrowsLotAuction) EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 5), 5); auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 5), 100, 150, 5); + input.minimumPurchaseQuantity = UINT64_MAX; const auto output = nostromo.createAuction(seller, input); ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); const auto auction = nostromo.getAuction(output.auctionIndex).auction; EXPECT_EQ(auction.core.quantityForSale, 1ULL); - EXPECT_EQ(auction.core.minimumPurchaseQuantity, 1ULL); + EXPECT_EQ(auction.core.minimumPurchaseQuantity, 0ULL); EXPECT_EQ(auction.core.initialPrice, 100ULL); EXPECT_EQ(auction.core.salePrice, 150ULL); EXPECT_EQ(auction.core.minimumBidIncrement, 5ULL); @@ -975,6 +977,62 @@ TEST(ContractNostromoAuction, CreateStandardSingleAssetAuctionEscrowsLotAuction) 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.placeBidWithFundedReward(bidder, createOutput.auctionIndex, 10, 10, 100); + EXPECT_EQ(acceptedBid.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(acceptedBid.escrowedAmount, 100ULL); +} + TEST(ContractNostromoAuction, CreateStandardAuctionSupportsFourLotEntriesAuction) { ContractTestingNOST nostromo; @@ -1382,10 +1440,6 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) invalidBatchBuyNow.buyNowPrice = 100; EXPECT_EQ(nostromo.createAuction(seller, invalidBatchBuyNow).errorCode, NOST::EAuctionError::InvalidInput); - auto invalidStandardMinimumPurchase = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); - invalidStandardMinimumPurchase.minimumPurchaseQuantity = 2; - EXPECT_EQ(nostromo.createAuction(seller, invalidStandardMinimumPurchase).errorCode, NOST::EAuctionError::InvalidInput); - auto invalidStandardIncrement = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); invalidStandardIncrement.minimumBidIncrement = 0; EXPECT_EQ(nostromo.createAuction(seller, invalidStandardIncrement).errorCode, NOST::EAuctionError::InvalidInput); @@ -1891,6 +1945,76 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF 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); + NOST::AuctionRevenueBreakdown expectedRevenue{}; + nostromo.calculateAuctionRevenueBreakdown(200ULL, expectedRevenue); + const sint64 sellerBalanceBefore = getBalance(seller); + nostromo.seedUser(firstBidder, 200); + nostromo.seedUser(secondBidder, 150); + const sint64 secondBidderBalanceBefore = getBalance(secondBidder); + + ASSERT_EQ(nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 10, 20, 200).errorCode, + NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBidWithFundedReward(secondBidder, createOutput.auctionIndex, 10, 15, 150).errorCode, + NOST::EAuctionError::Success); + + 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, 1); + EXPECT_EQ(firstParticipant.participantData.allocatedQuantity, 10ULL); + EXPECT_EQ(secondParticipant.participantData.allocatedQuantity, 0ULL); + EXPECT_EQ(secondParticipant.participantData.isWinningBid, 0u); + EXPECT_EQ(secondParticipant.participantData.escrowedAmount, 0ULL); + 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, expectedRevenue.sellerPayout); + } + + { + 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.placeBid(firstBidder, createOutput.auctionIndex, 10, 20, 200).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(partialBidder, createOutput.auctionIndex, 15, 15, 225).errorCode, NOST::EAuctionError::Success); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + + const auto partialParticipant = nostromo.getParticipant(createOutput.auctionIndex, partialBidder); + ASSERT_EQ(partialParticipant.found, 1); + EXPECT_EQ(partialParticipant.participantData.allocatedQuantity, 12ULL); + EXPECT_EQ(partialParticipant.participantData.isWinningBid, 1u); + EXPECT_EQ(nostromo.managedShares(asset, partialBidder), 12); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.allocatedQuantity, 22ULL); + } } TEST(ContractNostromoAuction, BatchFinalizationRefundsLosingBidsAndTieBreaksByBidTimeAuction) From 19418479b44811ca5dd0779dd7390de6587bf897 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 25 Jun 2026 17:21:57 +0300 Subject: [PATCH 48/59] Add minimum price and bid increment for Standard Auctions; implement batch auction bid availability check Change hashmap to array --- src/contracts/Nostromo.h | 646 +++++++++++++++++++++++++++++-------- test/contract_nostromo.cpp | 461 ++++++++++++++++++++------ 2 files changed, 862 insertions(+), 245 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 21b03cf06..cde272d8b 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -16,6 +16,8 @@ constexpr uint64 NOST_AUCTION_HISTORY_NUM = 1024; 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; +// 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. @@ -80,6 +82,10 @@ constexpr uint64 NOST_MICROSECONDS_PER_SECOND = 1000000ULL; 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. @@ -158,23 +164,22 @@ struct NOST : public ContractBase BidTooLow, PrivateAuctionAccessDenied, AuctionPaused, - AuctionIndexExhausted - }; - - struct AuctionParticipantKey - { - uint64 auctionIndex; - id participant; - - bool operator==(const AuctionParticipantKey& rhs) const { return auctionIndex == rhs.auctionIndex && participant == rhs.participant; } + AuctionIndexExhausted, + QuantityUnavailable }; /** - * @brief Stores the active bid state of one wallet in one auction. + * @brief Stores one bid slot in one auction. * @note The same struct is shared by batch and standard auctions. */ struct AuctionParticipantData { + /** @brief Auction that owns this bid slot. */ + uint64 auctionIndex; + + /** @brief Monotonic bid sequence inside the auction, used for FIFO tie-breaks. */ + uint64 bidIndex; + /** @brief Amount currently locked in escrow for the participant bid. */ uint64 escrowedAmount; @@ -187,11 +192,17 @@ struct NOST : public ContractBase /** @brief Offered price per asset in a batch auction, or total offered price for the whole lot in a standard auction. */ uint64 bidAmount; + /** @brief Wallet that owns this participant record. */ + id participant; + /** @brief Timestamp of the participant's latest accepted bid. */ DateAndTime lastBidTime; - /** @brief Wallet that owns this participant record. */ - id participant; + /** @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; @@ -278,6 +289,12 @@ struct NOST : public ContractBase /** @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; @@ -399,7 +416,7 @@ struct NOST : public ContractBase uint64 closedAuctionHistoryCounter; HashMap auctionList; - HashMap participants; + Array participants; }; /** @brief Input payload used to create a Batch Auction or Standard Auction in the Auction House. */ @@ -963,6 +980,29 @@ struct NOST : public ContractBase 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 { @@ -1057,7 +1097,6 @@ struct NOST : public ContractBase struct GetterScan_locals { AuctionData auction; - AuctionParticipantKey participantKey; AuctionParticipantData participantData; AuctionSummary auctionSummary; ParticipantSummary participantSummary; @@ -1066,7 +1105,7 @@ struct NOST : public ContractBase uint64 boundedLimit; uint64 metadataIndex; uint64 requestedIndex; - sint64 participantMapIndex; + uint64 participantSlotIndex; uint8 metadataMatches; }; @@ -1081,6 +1120,43 @@ struct NOST : public ContractBase using GetAuctionCountBySeller_locals = GetterScan_locals; using GetAuctionAtCreationSnapshot_locals = GetterScan_locals; + struct GetAuctionParticipant_locals + { + AuctionParticipantData participantData; + uint64 participantSlotIndex; + uint64 bestParticipantSlotIndex; + uint8 bestParticipantFound; + }; + + /** @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 participantIndex; + uint8 lowestWinningPriceFound; + }; + + struct GetBatchAuctionBidAvailability_locals + { + ComputeBatchBidAvailability_input computeBatchBidAvailabilityInput; + }; + /** @brief Internal input used to verify whether the invocator satisfies any private asset requirement. */ struct HasRequiredAccessAsset_input { @@ -1317,13 +1393,21 @@ struct NOST : public ContractBase { AuctionData auction; AuctionParticipantData participantData; - AuctionParticipantKey participantKey; + AuctionParticipantData worstParticipantData; + ComputeBatchBidAvailability_input computeBatchBidAvailabilityInput; + ComputeBatchBidAvailability_output computeBatchBidAvailabilityOutput; RecomputeBatchHighestBid_input recomputeBatchHighestBidInput; RecomputeBatchHighestBid_output recomputeBatchHighestBidOutput; - uint64 previousEscrow; + uint64 activeQuantity; + uint64 displacedQuantity; + uint64 displacedRefund; + uint64 excessQuantity; uint64 requiredEscrow; - uint8 mustRecomputeHighestBid; - uint8 participantExists; + uint64 participantIndex; + uint64 freeParticipantSlotIndex; + uint64 worstParticipantSlotIndex; + uint8 worstParticipantFound; + uint8 freeParticipantSlotFound; }; struct RecomputeBatchHighestBid_locals @@ -1331,9 +1415,8 @@ struct NOST : public ContractBase AuctionData auction; AuctionParticipantData participantData; AuctionParticipantData bestParticipantData; - AuctionParticipantKey participantKey; - AuctionParticipantKey bestParticipantKey; - sint64 participantIndex; + uint64 participantIndex; + uint64 bestParticipantSlotIndex; uint8 bestParticipantFound; }; @@ -1374,14 +1457,16 @@ struct NOST : public ContractBase AuctionData auction; AuctionParticipantData participantData; AuctionParticipantData previousHighestBidderData; - AuctionParticipantKey participantKey; - AuctionParticipantKey highestBidderKey; FinalizeStandardAuction_input finalizeStandardAuctionInput; FinalizeStandardAuction_output finalizeStandardAuctionOutput; uint64 previousEscrow; uint64 requiredEscrow; + uint64 participantSlotIndex; + uint64 highestBidderSlotIndex; + uint64 freeParticipantSlotIndex; uint8 participantExists; uint8 highestBidderExists; + uint8 freeParticipantSlotFound; uint8 finalizeImmediately; }; @@ -1473,8 +1558,6 @@ struct NOST : public ContractBase AuctionData auction; AuctionParticipantData participantData; AuctionParticipantData bestParticipantData; - AuctionParticipantKey participantKey; - AuctionParticipantKey bestParticipantKey; AuctionAssetEntry batchLotItem; DistributeAuctionRevenue_input distributeAuctionRevenueInput; DistributeAuctionRevenue_output distributeAuctionRevenueOutput; @@ -1486,7 +1569,8 @@ struct NOST : public ContractBase uint64 soldQuantity; uint64 totalGrossAmount; uint64 lotItemIndex; - sint64 participantIndex; + uint64 participantIndex; + uint64 bestParticipantSlotIndex; uint8 bestParticipantFound; uint8 lotItemFound; }; @@ -1495,11 +1579,11 @@ struct NOST : public ContractBase { AuctionData auction; AuctionParticipantData highestBidderData; - AuctionParticipantKey highestBidderKey; RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; DistributeAuctionRevenue_input distributeAuctionRevenueInput; DistributeAuctionRevenue_output distributeAuctionRevenueOutput; + uint64 highestBidderSlotIndex; uint8 highestBidderExists; uint8 lotSold; }; @@ -1522,9 +1606,9 @@ struct NOST : public ContractBase { AuctionData auction; AuctionParticipantData highestBidderData; - AuctionParticipantKey highestBidderKey; RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; + uint64 highestBidderSlotIndex; uint8 highestBidderExists; }; @@ -1580,7 +1664,6 @@ struct NOST : public ContractBase { AuctionData auction; AuctionParticipantData participantData; - AuctionParticipantKey participantKey; NostromoProcedureLog log; RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; @@ -1588,7 +1671,7 @@ struct NOST : public ContractBase DistributeAuctionServiceFee_output distributeAuctionServiceFeeOutput; DateAndTime currentDate; uint64 cancellationBaseAmount; - sint64 participantIndex; + uint64 participantIndex; }; struct ResolvePendingStandardAuction_locals @@ -1703,6 +1786,7 @@ struct NOST : public ContractBase REGISTER_USER_FUNCTION(GetLatestAuctionIndex, 16); REGISTER_USER_FUNCTION(GetAuctionCountBySeller, 17); REGISTER_USER_FUNCTION(GetAuctionAtCreationSnapshot, 18); + REGISTER_USER_FUNCTION(GetBatchAuctionBidAvailability, 19); } INITIALIZE() @@ -1794,7 +1878,6 @@ struct NOST : public ContractBase END_EPOCH() { state.mut().auctionList.cleanupIfNeeded(); - state.mut().participants.cleanupIfNeeded(); } END_TICK_WITH_LOCALS() @@ -2221,37 +2304,38 @@ struct NOST : public ContractBase return; } - for (locals.participantIndex = state.get().participants.nextElementIndex(NULL_INDEX); locals.participantIndex != NULL_INDEX; - locals.participantIndex = state.get().participants.nextElementIndex(locals.participantIndex)) + for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) { - locals.participantKey = state.get().participants.key(locals.participantIndex); - if (locals.participantKey.auctionIndex != input.auctionIndex) + locals.participantData = state.get().participants.get(locals.participantIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) { continue; } - locals.participantData = state.get().participants.value(locals.participantIndex); - if (locals.participantData.escrowedAmount == 0) + 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.lastBidTime < locals.bestParticipantData.lastBidTime)) + (locals.participantData.lastBidTime < locals.bestParticipantData.lastBidTime || + (locals.participantData.lastBidTime == locals.bestParticipantData.lastBidTime && + locals.participantData.bidIndex < locals.bestParticipantData.bidIndex)))) { locals.bestParticipantFound = 1; locals.bestParticipantData = locals.participantData; - locals.bestParticipantKey = locals.participantKey; + locals.bestParticipantSlotIndex = locals.participantIndex; } } if (locals.bestParticipantFound) { - locals.auction.core.highestBidder = locals.bestParticipantKey.participant; + 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 { @@ -2259,11 +2343,126 @@ struct NOST : public ContractBase 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); } + 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; + + 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; + } + + 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); + } + } + + if (locals.salePriorityQuantity >= locals.auction.core.quantityForSale) + { + output.availableQuantity = 0; + } + else + { + output.availableQuantity = locals.auction.core.quantityForSale - locals.salePriorityQuantity; + } + + if (output.availableQuantity >= locals.auction.core.minimumPurchaseQuantity) + { + output.minimumBidPrice = locals.auction.core.salePrice; + output.isAcceptingBids = 1; + } + else + { + 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; + } + + 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); + } + } + + if (locals.priorityQuantity >= locals.auction.core.quantityForSale) + { + output.availableQuantity = 0; + return; + } + + output.availableQuantity = locals.auction.core.quantityForSale - locals.priorityQuantity; + } + PRIVATE_PROCEDURE_WITH_LOCALS(ProcessBatchBid) { output.escrowedAmount = 0; @@ -2272,6 +2471,7 @@ struct NOST : public ContractBase output.success = 0; if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { + output.refundedAmount = static_cast(qpi.invocationReward()); output.errorCode = EAuctionError::AuctionNotFound; return; } @@ -2285,22 +2485,53 @@ struct NOST : public ContractBase 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; + } locals.requiredEscrow = smul(input.effectiveQuantity, input.bidAmount); if (static_cast(qpi.invocationReward()) < locals.requiredEscrow) { + output.refundedAmount = static_cast(qpi.invocationReward()); output.errorCode = EAuctionError::InsufficientFunds; return; } - locals.participantKey = {input.auctionIndex, qpi.invocator()}; - locals.participantExists = state.get().participants.get(locals.participantKey, locals.participantData); - locals.previousEscrow = locals.participantExists ? locals.participantData.escrowedAmount : 0; - locals.mustRecomputeHighestBid = locals.participantExists && locals.auction.core.highestBidder == qpi.invocator() && - input.bidAmount <= locals.auction.core.highestBidPrice; + 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.requiredEscrow; locals.participantData.requestedQuantity = input.effectiveQuantity; @@ -2308,15 +2539,11 @@ struct NOST : public ContractBase locals.participantData.bidAmount = input.bidAmount; locals.participantData.lastBidTime = input.currentDate; locals.participantData.participant = qpi.invocator(); - locals.participantData.isWinningBid = 0; - - if (input.bidAmount > locals.auction.core.highestBidPrice) - { - locals.auction.core.highestBidder = qpi.invocator(); - locals.auction.core.highestBidPrice = input.bidAmount; - locals.auction.core.highestBidQuantity = input.effectiveQuantity; - locals.auction.core.highestBidAmount = locals.requiredEscrow; - } + locals.participantData.auctionIndex = input.auctionIndex; + locals.participantData.bidIndex = locals.auction.core.nextBidIndex; + locals.participantData.isUsed = 1; + locals.participantData.isActive = 1; + locals.participantData.isWinningBid = 1; locals.auction.core.lastBidAt = input.currentDate; if ((locals.auction.core.auctionDurationSeconds - input.elapsedSeconds) <= NOST_AUCTION_EXTENSION_SECONDS) @@ -2324,23 +2551,87 @@ struct NOST : public ContractBase locals.auction.core.auctionDurationSeconds = sadd(locals.auction.core.auctionDurationSeconds, NOST_AUCTION_EXTENSION_SECONDS); } - if (state.mut().participants.set(locals.participantKey, locals.participantData) == NULL_INDEX) - { - output.errorCode = EAuctionError::StorageFull; - return; - } + 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); - if (locals.mustRecomputeHighestBid) + + locals.activeQuantity = 0; + for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) { - locals.recomputeBatchHighestBidInput.auctionIndex = input.auctionIndex; - CALL(RecomputeBatchHighestBid, locals.recomputeBatchHighestBidInput, locals.recomputeBatchHighestBidOutput); + 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); + } } - if (locals.previousEscrow > 0) + while (locals.activeQuantity > locals.auction.core.quantityForSale) { - qpi.transfer(qpi.invocator(), locals.previousEscrow); - output.refundedAmount = sadd(output.refundedAmount, locals.previousEscrow); + locals.worstParticipantFound = 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.worstParticipantFound || locals.participantData.bidAmount < locals.worstParticipantData.bidAmount || + (locals.participantData.bidAmount == locals.worstParticipantData.bidAmount && + (locals.participantData.lastBidTime > locals.worstParticipantData.lastBidTime || + (locals.participantData.lastBidTime == locals.worstParticipantData.lastBidTime && + 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); + 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; + } + + state.mut().participants.set(locals.worstParticipantSlotIndex, locals.worstParticipantData); + if (locals.displacedRefund > 0) + { + qpi.transfer(locals.worstParticipantData.participant, locals.displacedRefund); + output.refundedAmount = sadd(output.refundedAmount, locals.displacedRefund); + } + locals.activeQuantity -= locals.displacedQuantity; } + + locals.recomputeBatchHighestBidInput.auctionIndex = input.auctionIndex; + CALL(RecomputeBatchHighestBid, locals.recomputeBatchHighestBidInput, locals.recomputeBatchHighestBidOutput); + if (static_cast(qpi.invocationReward()) > locals.requiredEscrow) { qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - locals.requiredEscrow); @@ -2359,6 +2650,8 @@ struct NOST : public ContractBase 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; @@ -2393,14 +2686,36 @@ struct NOST : public ContractBase return; } - locals.participantKey = {input.auctionIndex, qpi.invocator()}; - locals.participantExists = state.get().participants.get(locals.participantKey, locals.participantData); + 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 && state.get().participants.population() >= state.get().participants.capacity()) + 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; @@ -2408,20 +2723,32 @@ struct NOST : public ContractBase 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); + } - if (!isZero(locals.auction.core.highestBidder)) + locals.highestBidderSlotIndex = locals.auction.core.highestBidSlotIndex; + if (locals.highestBidderSlotIndex < state.get().participants.capacity()) { - locals.highestBidderKey = {locals.auction.core.auctionIndex, locals.auction.core.highestBidder}; - locals.highestBidderExists = state.get().participants.get(locals.highestBidderKey, locals.previousHighestBidderData); + 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()) { qpi.transfer(locals.previousHighestBidderData.participant, locals.previousHighestBidderData.escrowedAmount); output.refundedAmount = sadd(output.refundedAmount, locals.previousHighestBidderData.escrowedAmount); locals.previousHighestBidderData.escrowedAmount = 0; + locals.previousHighestBidderData.requestedQuantity = 0; + locals.previousHighestBidderData.isActive = 0; locals.previousHighestBidderData.isWinningBid = 0; - state.mut().participants.replace(locals.highestBidderKey, locals.previousHighestBidderData); + state.mut().participants.set(locals.highestBidderSlotIndex, locals.previousHighestBidderData); } locals.participantData.isWinningBid = 1; @@ -2429,6 +2756,7 @@ struct NOST : public ContractBase 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) @@ -2440,11 +2768,7 @@ struct NOST : public ContractBase locals.finalizeImmediately = 1; } - if (state.mut().participants.set(locals.participantKey, locals.participantData) == NULL_INDEX) - { - output.errorCode = EAuctionError::StorageFull; - return; - } + state.mut().participants.set(locals.participantSlotIndex, locals.participantData); state.mut().auctionList.replace(input.auctionIndex, locals.auction); if (locals.previousEscrow > 0) @@ -2580,33 +2904,34 @@ struct NOST : public ContractBase return; } - // Once supply falls below the minimum, no valid allocation remains; the common refund path returns all residual escrow and assets. + // Bids were valid when submitted; final fragments may be smaller than `minimumPurchaseQuantity` after displacement. locals.remainingQuantity = locals.auction.core.quantityForSale; - while (locals.remainingQuantity >= locals.auction.core.minimumPurchaseQuantity) + while (locals.remainingQuantity > 0) { locals.bestParticipantFound = 0; - locals.participantIndex = state.get().participants.nextElementIndex(NULL_INDEX); + locals.participantIndex = 0; // Scan all bids for this auction to find the highest price, using earlier bid time as the tie-breaker. - while (locals.participantIndex != NULL_INDEX) + while (locals.participantIndex < state.get().participants.capacity()) { - locals.participantKey = state.get().participants.key(locals.participantIndex); - if (locals.participantKey.auctionIndex == input.auctionIndex) + locals.participantData = state.get().participants.get(locals.participantIndex); + if (locals.participantData.isUsed && locals.participantData.auctionIndex == input.auctionIndex) { - locals.participantData = state.get().participants.value(locals.participantIndex); - if (locals.participantData.escrowedAmount > 0) + 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.lastBidTime < locals.bestParticipantData.lastBidTime)) + (locals.participantData.lastBidTime < locals.bestParticipantData.lastBidTime || + (locals.participantData.lastBidTime == locals.bestParticipantData.lastBidTime && + locals.participantData.bidIndex < locals.bestParticipantData.bidIndex)))) { locals.bestParticipantFound = 1; locals.bestParticipantData = locals.participantData; - locals.bestParticipantKey = locals.participantKey; + locals.bestParticipantSlotIndex = locals.participantIndex; } } } - locals.participantIndex = state.get().participants.nextElementIndex(locals.participantIndex); + ++locals.participantIndex; } if (!locals.bestParticipantFound) @@ -2643,27 +2968,28 @@ struct NOST : public ContractBase // Clear the processed escrow so the same bid cannot participate in later iterations. locals.bestParticipantData.escrowedAmount = 0; - state.mut().participants.replace(locals.bestParticipantKey, locals.bestParticipantData); + locals.bestParticipantData.isActive = 0; + 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 = state.get().participants.nextElementIndex(NULL_INDEX); - while (locals.participantIndex != NULL_INDEX) + locals.participantIndex = 0; + while (locals.participantIndex < state.get().participants.capacity()) { - locals.participantKey = state.get().participants.key(locals.participantIndex); - if (locals.participantKey.auctionIndex == input.auctionIndex) + locals.participantData = state.get().participants.get(locals.participantIndex); + if (locals.participantData.isUsed && locals.participantData.auctionIndex == input.auctionIndex) { - locals.participantData = state.get().participants.value(locals.participantIndex); if (locals.participantData.escrowedAmount > 0) { qpi.transfer(locals.participantData.participant, locals.participantData.escrowedAmount); locals.participantData.escrowedAmount = 0; locals.participantData.allocatedQuantity = 0; locals.participantData.isWinningBid = 0; - state.mut().participants.replace(locals.participantKey, locals.participantData); } + locals.participantData.isActive = 0; + state.mut().participants.set(locals.participantIndex, locals.participantData); } - locals.participantIndex = state.get().participants.nextElementIndex(locals.participantIndex); + ++locals.participantIndex; } // Return any unsold batch quantity to the seller when demand did not consume the entire lot. @@ -2685,6 +3011,7 @@ struct NOST : public ContractBase 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().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); addClosedAuctionToHistory(state, locals.auction.core.auctionIndex); output.success = 1; @@ -2706,10 +3033,12 @@ struct NOST : public ContractBase return; } - if (!isZero(locals.auction.core.highestBidder)) + locals.highestBidderSlotIndex = locals.auction.core.highestBidSlotIndex; + if (locals.highestBidderSlotIndex < state.get().participants.capacity()) { - locals.highestBidderKey = {locals.auction.core.auctionIndex, locals.auction.core.highestBidder}; - locals.highestBidderExists = state.get().participants.get(locals.highestBidderKey, locals.highestBidderData); + locals.highestBidderData = state.get().participants.get(locals.highestBidderSlotIndex); + locals.highestBidderExists = locals.highestBidderData.isUsed && locals.highestBidderData.isActive && + locals.highestBidderData.auctionIndex == input.auctionIndex; } if (locals.highestBidderExists && locals.highestBidderData.escrowedAmount > 0) @@ -2728,7 +3057,8 @@ struct NOST : public ContractBase locals.highestBidderData.allocatedQuantity = locals.auction.core.quantityForSale; locals.highestBidderData.isWinningBid = 1; locals.highestBidderData.escrowedAmount = 0; - state.mut().participants.replace(locals.highestBidderKey, locals.highestBidderData); + locals.highestBidderData.isActive = 0; + state.mut().participants.set(locals.highestBidderSlotIndex, locals.highestBidderData); locals.auction.core.allocatedQuantity = locals.auction.core.quantityForSale; locals.lotSold = 1; } @@ -2749,6 +3079,7 @@ struct NOST : public ContractBase 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); addClosedAuctionToHistory(state, locals.auction.core.auctionIndex); output.success = 1; @@ -2770,10 +3101,12 @@ struct NOST : public ContractBase return; } - if (!isZero(locals.auction.core.highestBidder)) + locals.highestBidderSlotIndex = locals.auction.core.highestBidSlotIndex; + if (locals.highestBidderSlotIndex < state.get().participants.capacity()) { - locals.highestBidderKey = {locals.auction.core.auctionIndex, locals.auction.core.highestBidder}; - locals.highestBidderExists = state.get().participants.get(locals.highestBidderKey, locals.highestBidderData); + locals.highestBidderData = state.get().participants.get(locals.highestBidderSlotIndex); + locals.highestBidderExists = locals.highestBidderData.isUsed && locals.highestBidderData.isActive && + locals.highestBidderData.auctionIndex == input.auctionIndex; } if (locals.highestBidderExists && locals.highestBidderData.escrowedAmount > 0) @@ -2782,8 +3115,9 @@ struct NOST : public ContractBase output.refundedAmount = locals.highestBidderData.escrowedAmount; locals.highestBidderData.escrowedAmount = 0; locals.highestBidderData.allocatedQuantity = 0; + locals.highestBidderData.isActive = 0; locals.highestBidderData.isWinningBid = 0; - state.mut().participants.replace(locals.highestBidderKey, locals.highestBidderData); + state.mut().participants.set(locals.highestBidderSlotIndex, locals.highestBidderData); } locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; @@ -2795,6 +3129,7 @@ struct NOST : public ContractBase 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().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); @@ -3066,6 +3401,7 @@ struct NOST : public ContractBase 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; for (locals.requiredAccessAssetIndex = 0; locals.requiredAccessAssetIndex < input.requiredAccessAssets.capacity(); ++locals.requiredAccessAssetIndex) { @@ -3336,19 +3672,6 @@ struct NOST : public ContractBase return; } - if (locals.auction.core.highestBidAmount > 0) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::Forbidden; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, - output.cancellationFee); - logProcedureResult(locals.log); - return; - } - locals.cancellationBaseAmount = locals.auction.core.salePrice; if (locals.auction.core.type == EAuctionType::Batch) { @@ -3370,24 +3693,23 @@ struct NOST : public ContractBase return; } - locals.participantIndex = state.get().participants.nextElementIndex(NULL_INDEX); - while (locals.participantIndex != NULL_INDEX) + locals.participantIndex = 0; + while (locals.participantIndex < state.get().participants.capacity()) { - locals.participantKey = state.get().participants.key(locals.participantIndex); - if (locals.participantKey.auctionIndex == input.auctionIndex) + locals.participantData = state.get().participants.get(locals.participantIndex); + if (locals.participantData.isUsed && locals.participantData.auctionIndex == input.auctionIndex) { - locals.participantData = state.get().participants.value(locals.participantIndex); if (locals.participantData.escrowedAmount > 0) { qpi.transfer(locals.participantData.participant, locals.participantData.escrowedAmount); output.refundedAmount = sadd(output.refundedAmount, locals.participantData.escrowedAmount); } - state.mut().participants.removeByKey(locals.participantKey); + locals.participantData = {}; + state.mut().participants.set(locals.participantIndex, locals.participantData); } - locals.participantIndex = state.get().participants.nextElementIndex(locals.participantIndex); + ++locals.participantIndex; } - state.mut().participants.cleanupIfNeeded(); locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.core.seller; @@ -3396,6 +3718,12 @@ struct NOST : public ContractBase 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().auctionList.replace(input.auctionIndex, locals.auction); addClosedAuctionToHistory(state, locals.auction.core.auctionIndex); @@ -3673,9 +4001,29 @@ struct NOST : public ContractBase * @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(GetAuctionParticipant) + PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionParticipant) { - output.found = state.get().participants.get({input.auctionIndex, input.participant}, output.participantData) ? 1 : 0; + output.found = 0; + locals.bestParticipantFound = 0; + 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) + { + continue; + } + + if (!locals.bestParticipantFound || locals.participantData.lastBidTime > output.participantData.lastBidTime || + (locals.participantData.lastBidTime == output.participantData.lastBidTime && + locals.participantData.bidIndex > output.participantData.bidIndex)) + { + locals.bestParticipantFound = 1; + locals.bestParticipantSlotIndex = locals.participantSlotIndex; + output.participantData = locals.participantData; + output.found = 1; + } + } } /** @@ -3744,7 +4092,6 @@ struct NOST : public ContractBase PUBLIC_FUNCTION_WITH_LOCALS(GetContractStats) { output.stats.totalAuctionsCreated = state.get().totalAuctionsCreated; - output.stats.participantCount = state.get().participants.population(); output.stats.closedAuctionHistoryCounter = state.get().closedAuctionHistoryCounter; output.stats.auctionShareholderDividendPool = state.get().auctionShareholderDividendPool; output.stats.qxTransferFee = state.get().qxTransferFee; @@ -3752,6 +4099,15 @@ struct NOST : public ContractBase output.stats.isAuctionTimerPaused = state.get().isAuctionTimerPaused; output.stats.isPostBeginEpochPauseArmed = state.get().isPostBeginEpochPauseArmed; + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) + { + locals.participantData = state.get().participants.get(locals.participantSlotIndex); + if (locals.participantData.isUsed) + { + output.stats.participantCount = sadd(output.stats.participantCount, 1ULL); + } + } + for (locals.auctionIndex = 0; locals.auctionIndex < state.get().totalAuctionsCreated; ++locals.auctionIndex) { if (!state.get().auctionList.get(locals.auctionIndex, locals.auction)) @@ -3885,17 +4241,15 @@ struct NOST : public ContractBase output.totalCount = 0; output.returnedCount = 0; locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); - for (locals.participantMapIndex = state.get().participants.nextElementIndex(NULL_INDEX); locals.participantMapIndex != NULL_INDEX; - locals.participantMapIndex = state.get().participants.nextElementIndex(locals.participantMapIndex)) + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) { - locals.participantKey = state.get().participants.key(locals.participantMapIndex); - if (locals.participantKey.auctionIndex != input.auctionIndex) + locals.participantData = state.get().participants.get(locals.participantSlotIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex) { continue; } if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) { - locals.participantData = state.get().participants.value(locals.participantMapIndex); fillParticipantSummary(locals.participantData, locals.participantSummary); output.participants.set(output.returnedCount, locals.participantSummary); output.returnedCount = sadd(output.returnedCount, 1ULL); @@ -3909,18 +4263,16 @@ struct NOST : public ContractBase output.totalCount = 0; output.returnedCount = 0; locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); - for (locals.participantMapIndex = state.get().participants.nextElementIndex(NULL_INDEX); locals.participantMapIndex != NULL_INDEX; - locals.participantMapIndex = state.get().participants.nextElementIndex(locals.participantMapIndex)) + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) { - locals.participantKey = state.get().participants.key(locals.participantMapIndex); - if (locals.participantKey.participant != input.participant) + locals.participantData = state.get().participants.get(locals.participantSlotIndex); + if (!locals.participantData.isUsed || locals.participantData.participant != input.participant) { continue; } if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) { - locals.participantData = state.get().participants.value(locals.participantMapIndex); - fillUserParticipationSummary(locals.participantKey.auctionIndex, locals.participantData, locals.userParticipationSummary); + fillUserParticipationSummary(locals.participantData.auctionIndex, locals.participantData, locals.userParticipationSummary); output.participations.set(output.returnedCount, locals.userParticipationSummary); output.returnedCount = sadd(output.returnedCount, 1ULL); } @@ -3967,6 +4319,17 @@ struct NOST : public ContractBase output.visibility = static_cast(locals.auction.core.visibility); } + /** + * @brief Returns current read-only guidance for the next valid Batch Auction bid. + * @note `PlaceBid` re-runs the same availability validation before accepting a bid. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetBatchAuctionBidAvailability) + { + locals.computeBatchBidAvailabilityInput.auctionIndex = input.auctionIndex; + locals.computeBatchBidAvailabilityInput.bidAmount = 0; + CALL(ComputeBatchBidAvailability, locals.computeBatchBidAvailabilityInput, output); + } + /** * @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. @@ -4109,7 +4472,8 @@ struct NOST : public ContractBase { quantityForSale = 0; resolvedMinimumPurchaseQuantity = 0; - if (minimumBidIncrement == 0 || salePrice == 0) + if (initialPrice < NOST_STANDARD_MIN_PRICE || salePrice < NOST_STANDARD_MIN_PRICE || + minimumBidIncrement < NOST_STANDARD_MIN_BID_INCREMENT) { return false; } diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index dc8595066..87391a4fd 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -456,6 +456,16 @@ class ContractTestingNOST : protected ContractTesting 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::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]); } @@ -564,7 +574,9 @@ class ContractTestingNOST : protected ContractTesting } static NOST::CreateAuction_input makeStandardAuctionInput(const Array& lot, - uint64 initialPrice = 100, uint64 salePrice = 150, uint64 minimumBidIncrement = 10, + 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{}; @@ -728,7 +740,7 @@ TEST(ContractNostromoAuction, AuctionIndexAndExpandedGetterSurfaceAuction) auto inputA = ContractTestingNOST::makeBatchAuctionInput(assetA, 3, 10); auto inputB = ContractTestingNOST::makeBatchAuctionInput(assetB, 2, 12); - auto inputC = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetC, 1), 100, 150, 10); + auto inputC = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetC, 1)); inputB.metadataIpfsCid.set(10, '2'); inputC.metadataIpfsCid.set(10, '3'); @@ -779,7 +791,8 @@ TEST(ContractNostromoAuction, AuctionIndexAndExpandedGetterSurfaceAuction) ASSERT_EQ(nostromo.placeBid(bidderA, createA.auctionIndex, 2, 11, 22).errorCode, NOST::EAuctionError::Success); ASSERT_EQ(nostromo.placeBid(bidderB, createA.auctionIndex, 1, 15, 15).errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(bidderA, createC.auctionIndex, 1, 100, 100).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); @@ -959,7 +972,7 @@ TEST(ContractNostromoAuction, CreateStandardSingleAssetAuctionEscrowsLotAuction) EXPECT_EQ(nostromo.issueAsset(seller, assetName, 5), 5); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 5), 5); - auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 5), 100, 150, 5); + auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 5)); input.minimumPurchaseQuantity = UINT64_MAX; const auto output = nostromo.createAuction(seller, input); @@ -968,9 +981,9 @@ TEST(ContractNostromoAuction, CreateStandardSingleAssetAuctionEscrowsLotAuction) 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, 100ULL); - EXPECT_EQ(auction.core.salePrice, 150ULL); - EXPECT_EQ(auction.core.minimumBidIncrement, 5ULL); + 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); @@ -1055,7 +1068,7 @@ TEST(ContractNostromoAuction, CreateStandardAuctionSupportsFourLotEntriesAuction const auto createOutput = nostromo.createAuction( seller, ContractTestingNOST::makeStandardAuctionInput( - ContractTestingNOST::makeLot({{assets[0], 3}, {assets[1], 3}, {assets[2], 3}, {assets[3], 3}}), 100, 100, 1)); + 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) { @@ -1063,7 +1076,8 @@ TEST(ContractNostromoAuction, CreateStandardAuctionSupportsFourLotEntriesAuction EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 3); } - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, 100, 100).errorCode, NOST::EAuctionError::Success); + 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; @@ -1444,14 +1458,34 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) invalidStandardIncrement.minimumBidIncrement = 0; EXPECT_EQ(nostromo.createAuction(seller, invalidStandardIncrement).errorCode, NOST::EAuctionError::InvalidInput); - auto invalidStandardPrice = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1), 200, 150, 10); + auto invalidStandardLowInitial = + ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE - 1, + NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_BID_INCREMENT); + EXPECT_EQ(nostromo.createAuction(seller, 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(nostromo.createAuction(seller, 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(nostromo.createAuction(seller, 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(nostromo.createAuction(seller, invalidStandardPrice).errorCode, NOST::EAuctionError::InvalidInput); auto invalidStandardSalePrice = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); invalidStandardSalePrice.salePrice = 0; EXPECT_EQ(nostromo.createAuction(seller, invalidStandardSalePrice).errorCode, NOST::EAuctionError::InvalidInput); - auto invalidStandardBuyNow = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1), 100, 150, 10, 140); + 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(nostromo.createAuction(seller, invalidStandardBuyNow).errorCode, NOST::EAuctionError::InvalidInput); auto privateWithoutGate = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); @@ -1617,13 +1651,24 @@ TEST(ContractNostromoAuction, PlaceBidRejectsWhenParticipantStorageIsFullAuction 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; - NOST::AuctionParticipantKey key{index + 100000ULL, participant.participant}; - ASSERT_NE(nostromo.stateData().participants.set(key, participant), NULL_INDEX); + participant.isUsed = 1; + participant.isActive = 1; + nostromo.stateData().participants.set(index, participant); } - ASSERT_EQ(nostromo.stateData().participants.population(), NOST_AUCTION_PARTICIPANT_NUM); + 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.placeBid(bidder, createOutput.auctionIndex, 1, 10, 10); EXPECT_EQ(output.errorCode, NOST::EAuctionError::StorageFull); @@ -1676,19 +1721,18 @@ TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAucti EXPECT_EQ(auction.core.highestBidAmount, 40ULL); const auto bidA2 = nostromo.placeBid(bidderA, createOutput.auctionIndex, 2, 14, 28); - EXPECT_EQ(bidA2.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(bidA2.escrowedAmount, 28ULL); - EXPECT_EQ(bidA2.refundedAmount, 40ULL); + EXPECT_EQ(bidA2.errorCode, NOST::EAuctionError::QuantityUnavailable); + EXPECT_EQ(bidA2.refundedAmount, 28ULL); auction = nostromo.getAuction(createOutput.auctionIndex).auction; - EXPECT_EQ(auction.core.highestBidder, bidderB); - EXPECT_EQ(auction.core.highestBidPrice, 15ULL); - EXPECT_EQ(auction.core.highestBidAmount, 45ULL); + 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, 28ULL); - EXPECT_EQ(participantA.participantData.bidAmount, 14ULL); + 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); @@ -1717,6 +1761,185 @@ TEST(ContractNostromoAuction, PlaceBatchBidExtendsAuctionNearEndAuction) 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.placeBid(bidderA, createOutput.auctionIndex, 8, 4, 32).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, 2, 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.placeBid(bidderB, createOutput.auctionIndex, 2, 2, 4); + 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.placeBid(bidderA, createOutput.auctionIndex, 10, 3, 30).errorCode, NOST::EAuctionError::Success); + auto availability = nostromo.getBatchAvailability(createOutput.auctionIndex); + EXPECT_EQ(availability.minimumBidPrice, 4ULL); + EXPECT_EQ(availability.availableQuantity, 0ULL); + EXPECT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, 2, 2).errorCode, NOST::EAuctionError::BidTooLow); + EXPECT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, 3, 3).errorCode, NOST::EAuctionError::BidTooLow); + + ASSERT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 3, 4, 12).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.placeBid(bidderA, createOutput.auctionIndex, 8, 4, 32).errorCode, NOST::EAuctionError::Success); + auto availability = nostromo.getBatchAvailability(createOutput.auctionIndex); + EXPECT_EQ(availability.minimumBidPrice, 5ULL); + 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, 4, 12).errorCode, NOST::EAuctionError::BidTooLow); + ASSERT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 3, 5, 15).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.placeBid(bidderA, createOutput.auctionIndex, 10, 3, 30).errorCode, NOST::EAuctionError::Success); + const auto improved = nostromo.placeBid(bidderA, createOutput.auctionIndex, 8, 4, 32); + EXPECT_EQ(improved.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(improved.refundedAmount, 24ULL); + + const auto participants = nostromo.getAuctionParticipants(createOutput.auctionIndex, 0, 64); + ASSERT_EQ(participants.totalCount, 2ULL); + uint64 quantityAtThree = 0; + uint64 quantityAtFour = 0; + for (uint64 index = 0; index < participants.returnedCount; ++index) + { + if (participants.participants.get(index).bidAmount == 3) + { + quantityAtThree = participants.participants.get(index).requestedQuantity; + } + if (participants.participants.get(index).bidAmount == 4) + { + quantityAtFour = participants.participants.get(index).requestedQuantity; + } + } + EXPECT_EQ(quantityAtThree, 2ULL); + EXPECT_EQ(quantityAtFour, 8ULL); + + nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); + EXPECT_EQ(nostromo.managedShares(asset, bidderA), 10); + EXPECT_EQ(nostromo.managedShares(asset, seller), 0); + } +} + +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.placeBid(bidder, closedBatchCreate.auctionIndex, 1, 2, 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; @@ -1729,26 +1952,31 @@ TEST(ContractNostromoAuction, PlaceStandardBidValidatesRefundsAndPauseAuction) 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), 100, 150, 10)); + 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, 100, 100); + 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, 99, 99); + 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, 100, 100); + 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, 100ULL); + EXPECT_EQ(openingBid.escrowedAmount, NOST_STANDARD_MIN_PRICE); - const auto lowIncrement = nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, 109, 109); + 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, 110, 110); + 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, 100ULL); + EXPECT_EQ(outbid.refundedAmount, NOST_STANDARD_MIN_PRICE); const auto bidderAState = nostromo.getParticipant(createOutput.auctionIndex, bidderA); const auto bidderBState = nostromo.getParticipant(createOutput.auctionIndex, bidderB); @@ -1756,27 +1984,34 @@ TEST(ContractNostromoAuction, PlaceStandardBidValidatesRefundsAndPauseAuction) ASSERT_EQ(bidderBState.found, 1); EXPECT_EQ(bidderAState.participantData.escrowedAmount, 0ULL); EXPECT_EQ(bidderAState.participantData.isWinningBid, 0u); - EXPECT_EQ(bidderBState.participantData.escrowedAmount, 110ULL); + 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, 130, 130); + 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, 110ULL); - EXPECT_EQ(bidderBImprove.escrowedAmount, 130ULL); + 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, 140, 140); + 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, 140, 140); + 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, 150, 150); + 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); } @@ -1851,14 +2086,17 @@ TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) EXPECT_EQ(nostromo.issueAsset(seller, assetName, 3), 3); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); - auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 3), 100, 150, 10, 180); + 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); NOST::AuctionRevenueBreakdown expectedRevenue{}; - nostromo.calculateAuctionRevenueBreakdown(180ULL, expectedRevenue); + nostromo.calculateAuctionRevenueBreakdown(NOST_STANDARD_MIN_PRICE + 800000ULL, expectedRevenue); 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, 180, 180); + 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; @@ -1892,7 +2130,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF ASSERT_EQ(nostromo.placeBid(bidderA, createOutput.auctionIndex, 3, 15, 45).errorCode, NOST::EAuctionError::Success); nostromo.setNow(2026, 1, 1, 9, 0, 1); - ASSERT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 3, 15, 45).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, 15, 45).errorCode, NOST::EAuctionError::Success); nostromo.setNow(2026, 1, 1, 9, 0, 2); ASSERT_EQ(nostromo.placeBid(bidderC, createOutput.auctionIndex, 2, 20, 40).errorCode, NOST::EAuctionError::Success); @@ -1969,18 +2207,15 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF ASSERT_EQ(nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 10, 20, 200).errorCode, NOST::EAuctionError::Success); ASSERT_EQ(nostromo.placeBidWithFundedReward(secondBidder, createOutput.auctionIndex, 10, 15, 150).errorCode, - NOST::EAuctionError::Success); + 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, 1); + ASSERT_EQ(secondParticipant.found, 0); EXPECT_EQ(firstParticipant.participantData.allocatedQuantity, 10ULL); - EXPECT_EQ(secondParticipant.participantData.allocatedQuantity, 0ULL); - EXPECT_EQ(secondParticipant.participantData.isWinningBid, 0u); - EXPECT_EQ(secondParticipant.participantData.escrowedAmount, 0ULL); EXPECT_EQ(nostromo.managedShares(asset, firstBidder), 10); EXPECT_EQ(nostromo.managedShares(asset, secondBidder), 0); EXPECT_EQ(nostromo.managedShares(asset, seller), 5); @@ -2004,16 +2239,15 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); ASSERT_EQ(nostromo.placeBid(firstBidder, createOutput.auctionIndex, 10, 20, 200).errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(partialBidder, createOutput.auctionIndex, 15, 15, 225).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBid(partialBidder, createOutput.auctionIndex, 15, 15, 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, 1); - EXPECT_EQ(partialParticipant.participantData.allocatedQuantity, 12ULL); - EXPECT_EQ(partialParticipant.participantData.isWinningBid, 1u); - EXPECT_EQ(nostromo.managedShares(asset, partialBidder), 12); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.allocatedQuantity, 22ULL); + ASSERT_EQ(partialParticipant.found, 0); + EXPECT_EQ(nostromo.managedShares(asset, partialBidder), 0); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.allocatedQuantity, 10ULL); } } @@ -2023,49 +2257,49 @@ TEST(ContractNostromoAuction, BatchFinalizationRefundsLosingBidsAndTieBreaksByBi const id seller(205, 206, 207, 208); const id earlierBidder(209, 210, 211, 212); const id laterBidder(213, 214, 215, 216); - const id lowerBidder(217, 218, 219, 220); + const id higherBidder(217, 218, 219, 220); const uint64 assetName = assetNameFromString("BATTIE"); const Asset asset{seller, assetName}; - EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); - EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); + 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, 1, 5)); + 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(lowerBidder, 100); + nostromo.seedUser(higherBidder, 100); const sint64 earlierBefore = getBalance(earlierBidder); const sint64 laterBefore = getBalance(laterBidder); - const sint64 lowerBefore = getBalance(lowerBidder); + const sint64 higherBefore = getBalance(higherBidder); ASSERT_EQ(nostromo.placeBidWithFundedReward(earlierBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, NOST::EAuctionError::Success); nostromo.setNow(2026, 1, 1, 9, 0, 1); ASSERT_EQ(nostromo.placeBidWithFundedReward(laterBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, NOST::EAuctionError::Success); nostromo.setNow(2026, 1, 1, 9, 0, 2); - ASSERT_EQ(nostromo.placeBidWithFundedReward(lowerBidder, createOutput.auctionIndex, 1, 9, 9).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBidWithFundedReward(higherBidder, createOutput.auctionIndex, 1, 11, 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 lower = nostromo.getParticipant(createOutput.auctionIndex, lowerBidder); + const auto higher = nostromo.getParticipant(createOutput.auctionIndex, higherBidder); ASSERT_EQ(earlier.found, 1); ASSERT_EQ(later.found, 1); - ASSERT_EQ(lower.found, 1); + ASSERT_EQ(higher.found, 1); EXPECT_EQ(earlier.participantData.allocatedQuantity, 1ULL); EXPECT_EQ(later.participantData.allocatedQuantity, 0ULL); - EXPECT_EQ(lower.participantData.allocatedQuantity, 0ULL); + EXPECT_EQ(higher.participantData.allocatedQuantity, 1ULL); EXPECT_EQ(earlier.participantData.escrowedAmount, 0ULL); EXPECT_EQ(later.participantData.escrowedAmount, 0ULL); - EXPECT_EQ(lower.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, lowerBidder), 0); + EXPECT_EQ(nostromo.managedShares(asset, higherBidder), 1); EXPECT_EQ(getBalance(earlierBidder), earlierBefore - 10); EXPECT_EQ(getBalance(laterBidder), laterBefore); - EXPECT_EQ(getBalance(lowerBidder), lowerBefore); + EXPECT_EQ(getBalance(higherBidder), higherBefore - 11); } TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) @@ -2078,8 +2312,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) 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), 100, 150, 10)); + const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); @@ -2102,8 +2335,7 @@ TEST(ContractNostromoAuction, WeeklyPauseShiftsActiveAuctionDeadlineAuction) 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), 100, 150, 10)); + 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); @@ -2131,8 +2363,7 @@ TEST(ContractNostromoAuction, EndTickSkipsAuctionProcessingAtBootstrapTimeAuctio 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), 100, 150, 10)); + 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); @@ -2155,10 +2386,14 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) 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), 100, 150, 10)); + 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, 120, 120).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; @@ -2222,18 +2457,19 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); NOST::AuctionRevenueBreakdown expectedRevenue{}; - nostromo.calculateAuctionRevenueBreakdown(10000ULL, expectedRevenue); + nostromo.calculateAuctionRevenueBreakdown(NOST_STANDARD_MIN_PRICE, expectedRevenue); const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedRevenue.shareholderDividendAmount); const auto createOutput = nostromo.createAuction( - seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), 10000, 10000, 10)); + seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); 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, 10000, 10000).errorCode, NOST::EAuctionError::Success); + 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); @@ -2246,7 +2482,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) if (routeMode != 0) { EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 10000ULL - expectedRevenue.sellerPayout); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, NOST_STANDARD_MIN_PRICE - expectedRevenue.sellerPayout); EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); } @@ -2272,10 +2508,14 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction 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), 100, 150, 10)); + 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, 120, 120).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); @@ -2299,17 +2539,21 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction 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), 100, 150, 10)); + 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, 500); + nostromo.seedUser(bidder, NOST_STANDARD_MIN_PRICE + 300000ULL); const sint64 bidderBeforeBid = getBalance(bidder); - ASSERT_EQ(nostromo.placeBidWithFundedReward(bidder, createOutput.auctionIndex, 1, 120, 120).errorCode, NOST::EAuctionError::Success); + 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, 120ULL); + 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); @@ -2333,10 +2577,14 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction 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), 100, 150, 10)); + 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, 120, 120).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); @@ -2414,8 +2662,8 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) 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), 8000, 10000, 10)); + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); const sint64 sellerBefore = getBalance(seller); const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); @@ -2423,20 +2671,20 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); NOST::AuctionServiceFeeBreakdown expectedBreakdown{}; - nostromo.calculateAuctionServiceFeeBreakdown(1000ULL, expectedBreakdown); + nostromo.calculateAuctionServiceFeeBreakdown(100000ULL, expectedBreakdown); const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedBreakdown.shareholderDividendAmount); - const auto cancelOutput = nostromo.cancelAuction(seller, createOutput.auctionIndex, 1000); + 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, 1000ULL); + 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); if (routeMode != 0) { EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 1000ULL); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 100000ULL); EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); } @@ -2513,8 +2761,8 @@ TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeR EXPECT_EQ(smallFeeNostromo.issueAsset(standardSeller, standardAssetName, 1), 1); EXPECT_EQ(smallFeeNostromo.transferShareManagementRightsToNostromo(standardSeller, standardAsset, 1), 1); - const auto standardCreateOutput = smallFeeNostromo.createAuction( - standardSeller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(standardAsset, 1), 19, 19, 1)); + const auto standardCreateOutput = + smallFeeNostromo.createAuction(standardSeller, ContractTestingNOST::makeBatchAuctionInput(standardAsset, 1, 19)); ASSERT_EQ(standardCreateOutput.errorCode, NOST::EAuctionError::Success); NOST::AuctionServiceFeeBreakdown expectedSmallBreakdown{}; @@ -2545,7 +2793,7 @@ TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeR } } -TEST(ContractNostromoAuction, CancelAuctionRejectsAfterBidAuction) +TEST(ContractNostromoAuction, CancelAuctionRefundsBidsAndFreesParticipantSlotsAuction) { ContractTestingNOST nostromo; const id seller(281, 282, 283, 284); @@ -2567,14 +2815,19 @@ TEST(ContractNostromoAuction, CancelAuctionRejectsAfterBidAuction) EXPECT_EQ(forbidden.errorCode, NOST::EAuctionError::Forbidden); const auto insufficient = nostromo.cancelAuction(seller, createOutput.auctionIndex, 1); - EXPECT_EQ(insufficient.errorCode, NOST::EAuctionError::Forbidden); + EXPECT_EQ(insufficient.errorCode, NOST::EAuctionError::InsufficientFunds); + const auto bidderBalanceBeforeCancel = getBalance(bidder); const auto success = nostromo.cancelAuction(seller, createOutput.auctionIndex, 2); - EXPECT_EQ(success.errorCode, NOST::EAuctionError::Forbidden); + EXPECT_EQ(success.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(success.refundedAmount, 12ULL); + EXPECT_EQ(getBalance(bidder) - bidderBalanceBeforeCancel, 12); + EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidder).found, 0); const auto closed = nostromo.cancelAuction(seller, createOutput.auctionIndex, 2); - EXPECT_EQ(closed.errorCode, NOST::EAuctionError::Forbidden); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Active); + EXPECT_EQ(closed.errorCode, NOST::EAuctionError::AuctionClosed); + EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Cancelled); + EXPECT_EQ(nostromo.managedShares(asset, seller), 2); } TEST(ContractNostromoAuction, CancelAuctionRejectsInvalidCasesWithoutBidsAuction) @@ -2783,7 +3036,7 @@ TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) const auto createOutput = nostromo.createAuction( seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1), cases[caseIndex].grossAmount, - cases[caseIndex].grossAmount, 1)); + cases[caseIndex].grossAmount, NOST_STANDARD_MIN_BID_INCREMENT)); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); const sint64 sellerBefore = getBalance(seller); const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); From a5e892b4c3cace28912ae284146f2de76623000e Mon Sep 17 00:00:00 2001 From: N-010 Date: Tue, 7 Jul 2026 18:55:46 +0300 Subject: [PATCH 49/59] Add batch auction creation fee and emergency pause configuration; implement fee reserve guard logic --- src/contracts/Nostromo.h | 484 ++++++++++++++++++++++++++++--- test/contract_nostromo.cpp | 578 +++++++++++++++++++++++++++++++++---- 2 files changed, 966 insertions(+), 96 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index cde272d8b..9e7db8bdf 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -32,6 +32,10 @@ constexpr uint64 NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM = 4; 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 Batch Auction and distributed at END_EPOCH, in qu. +constexpr uint64 NOST_PUBLIC_BATCH_AUCTION_CREATION_FEE = 100LL; +// Exclusive upper bound used to taper the accumulated Batch Auction bid fee against its normalized bid value, in qu. +constexpr uint64 NOST_BATCH_BID_FEE_CUTOFF = 101ULL; // 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. @@ -107,6 +111,10 @@ 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; struct NOST2 { @@ -123,7 +131,9 @@ struct NOST : public ContractBase ResolvePendingStandardAuction = 5, SetAuctionFees = 6, SetAuctionFeesByManagement = 7, - SetManagement = 8 + SetManagement = 8, + SetFeeReserveGuardConfig = 9, + SetEmergencyPause = 10 }; enum class EAuctionType : uint8 @@ -349,6 +359,9 @@ struct NOST : public ContractBase /** @brief Configured fee charged when creating a private auction. */ sint64 privateAuctionFee; + /** @brief Configured fee accumulated when creating a public Batch Auction and distributed at `END_EPOCH`. */ + uint64 batchAuctionCreationFee; + /** @brief Configured cancellation fee rate in basis points. */ uint64 auctionCancellationFeeBasisPoints; @@ -417,6 +430,27 @@ struct NOST : public ContractBase HashMap auctionList; Array participants; + + /** @brief Auction creation and Batch bid service fees accumulated during the epoch, distributed at `END_EPOCH`. */ + uint64 pendingServiceFeePool; + + /** @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. */ @@ -542,6 +576,9 @@ struct NOST : public ContractBase /** @brief Fee charged when a private auction is created. */ sint64 privateAuctionFee; + /** @brief Fee accumulated when a public Batch Auction is created and distributed at `END_EPOCH`; must not exceed `INT64_MAX`. */ + uint64 batchAuctionCreationFee; + /** @brief Cancellation fee rate in basis points. */ uint64 auctionCancellationFeeBasisPoints; @@ -582,6 +619,9 @@ struct NOST : public ContractBase /** @brief Fee charged when a private auction is created. */ sint64 privateAuctionFee; + /** @brief Fee accumulated when a public Batch Auction is created and distributed at `END_EPOCH`; must not exceed `INT64_MAX`. */ + uint64 batchAuctionCreationFee; + /** @brief Cancellation fee rate in basis points. */ uint64 auctionCancellationFeeBasisPoints; @@ -623,6 +663,35 @@ struct NOST : public ContractBase EAuctionError errorCode; }; + /** @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 SetFeeReserveGuardConfig_output + { + /** @brief Result code describing whether the guard configuration update succeeded. */ + EAuctionError errorCode; + }; + + /** @brief Input payload used by the takeover coordinator or management to manually pause or resume auction interactions. */ + struct SetEmergencyPause_input + { + /** @brief Set to `1` to activate the emergency pause or `0` to resume normal operation. */ + uint8 paused; + }; + + struct SetEmergencyPause_output + { + /** @brief Result code describing whether the emergency pause update succeeded. */ + EAuctionError errorCode; + }; + /** @brief Input payload used to fetch one auction from storage. */ struct GetAuctionByIndex_input { @@ -671,6 +740,42 @@ struct NOST : public ContractBase }; /** @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 + { + /** @brief Auction creation and Batch bid service fees accumulated during the epoch, distributed at `END_EPOCH`. */ + uint64 pendingServiceFeePool; + }; + + /** @brief Input payload used to read the current state of the execution fee reserve guard. */ + using GetFeeReserveGuardState_input = NoData; + + struct GetFeeReserveGuardState_output + { + /** @brief Live execution fee reserve value read from the system contract. */ + sint64 currentFeeReserve; + + /** @brief Execution fee reserve value recorded at the start of the current guard window. */ + sint64 feeReserveBaseline; + + /** @brief Start of the current guard window; invalid when the window has not been initialized. */ + DateAndTime feeReserveBaselineAt; + + /** @brief Timestamp at which the emergency pause was triggered; invalid when not paused. */ + DateAndTime emergencyPausedAt; + + /** @brief Configured drop in the execution fee reserve that triggers an emergency pause, in basis points. */ + uint64 dropBasisPoints; + + /** @brief Configured rolling window used to evaluate the execution fee reserve drop, in seconds. */ + uint64 windowSeconds; + + /** @brief Flag indicating whether an emergency pause is currently blocking auction interactions. */ + uint8 isEmergencyPaused; + }; + using GetAuctionFees_input = NoData; struct GetAuctionFees_output @@ -678,6 +783,9 @@ struct NOST : public ContractBase /** @brief Fee charged when a private auction is created. */ sint64 privateAuctionFee; + /** @brief Fee accumulated when a public Batch Auction is created and distributed at `END_EPOCH`. */ + uint64 batchAuctionCreationFee; + /** @brief Cancellation fee rate in basis points. */ uint64 auctionCancellationFeeBasisPoints; @@ -706,6 +814,33 @@ struct NOST : public ContractBase uint64 shareholderFeeBasisPointsTier4; }; + /** @brief Input for the arithmetic-only Batch Auction bid reward calculator. */ + struct CalculateBatchAuctionBidFee_input + { + /** @brief Number of assets requested by the prospective bid. */ + uint64 bidQuantity; + + /** @brief Total number of assets offered by the Batch Auction. */ + uint64 tokensForSale; + + /** @brief Prospective price per asset, in qu. */ + uint64 bidAmount; + }; + + /** @brief Escrow, accumulated fee, and total reward required by the Batch Auction bid arithmetic. */ + struct CalculateBatchAuctionBidFee_output + { + /** @brief Saturating product of `bidQuantity` and `bidAmount`. */ + uint64 escrowAmount; + + /** @brief Amount accumulated for distribution at `END_EPOCH` for an accepted bid: `max(101 - bidQuantity * floor(bidAmount / tokensForSale), + * 0)`. */ + uint64 fee; + + /** @brief Saturating sum of `escrowAmount` and `fee`. */ + uint64 requiredReward; + }; + /** * @brief Pure breakdown of one auction revenue split. * @note Runtime settlement and tests share this struct to keep fee arithmetic aligned. @@ -846,10 +981,12 @@ struct NOST : public ContractBase uint64 participantCount; uint64 closedAuctionHistoryCounter; uint64 auctionShareholderDividendPool; + uint64 pendingServiceFeePool; uint32 qxTransferFee; uint8 routeAllFeesToDevelopment; uint8 isAuctionTimerPaused; uint8 isPostBeginEpochPauseArmed; + uint8 isEmergencyPaused; }; using GetContractStats_input = NoData; @@ -1402,7 +1539,7 @@ struct NOST : public ContractBase uint64 displacedQuantity; uint64 displacedRefund; uint64 excessQuantity; - uint64 requiredEscrow; + CalculateBatchAuctionBidFee_output bidFeeCalculation; uint64 participantIndex; uint64 freeParticipantSlotIndex; uint64 worstParticipantSlotIndex; @@ -1701,6 +1838,10 @@ struct NOST : public ContractBase FinalizeBatchAuction_output finalizeBatchAuctionOutput; FinalizeStandardAuction_input finalizeStandardAuctionInput; FinalizeStandardAuction_output finalizeStandardAuctionOutput; + sint64 currentReserve; + sint64 reserveDrop; + uint64 guardElapsedSeconds; + uint64 guardDropThreshold; }; struct BEGIN_EPOCH_locals @@ -1709,6 +1850,12 @@ struct NOST : public ContractBase QX::Fees_output feesOutput; }; + struct END_EPOCH_locals + { + DistributeAuctionServiceFee_input distributeAuctionServiceFeeInput; + DistributeAuctionServiceFee_output distributeAuctionServiceFeeOutput; + }; + /** @brief Input payload used to move share management rights to another managing contract. */ struct TransferShareManagementRights_input { @@ -1757,6 +1904,16 @@ struct NOST : public ContractBase 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)); @@ -1767,6 +1924,8 @@ struct NOST : public ContractBase 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, 1); REGISTER_USER_FUNCTION(GetAuctionParticipant, 2); @@ -1787,11 +1946,15 @@ struct NOST : public ContractBase REGISTER_USER_FUNCTION(GetAuctionCountBySeller, 17); REGISTER_USER_FUNCTION(GetAuctionAtCreationSnapshot, 18); REGISTER_USER_FUNCTION(GetBatchAuctionBidAvailability, 19); + REGISTER_USER_FUNCTION(CalculateBatchAuctionBidFee, 20); + REGISTER_USER_FUNCTION(GetPendingServiceFeePool, 21); + REGISTER_USER_FUNCTION(GetFeeReserveGuardState, 22); } INITIALIZE() { state.mut().privateAuctionFee = NOST_DEFAULT_PRIVATE_AUCTION_FEE; + state.mut().batchAuctionCreationFee = NOST_PUBLIC_BATCH_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; @@ -1806,6 +1969,8 @@ struct NOST : public ContractBase 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, @@ -1828,6 +1993,7 @@ struct NOST : public ContractBase { // Initialize state.mut().privateAuctionFee = NOST_DEFAULT_PRIVATE_AUCTION_FEE; + state.mut().batchAuctionCreationFee = NOST_PUBLIC_BATCH_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; @@ -1839,6 +2005,8 @@ struct NOST : public ContractBase 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); @@ -1875,14 +2043,60 @@ struct NOST : public ContractBase } } - END_EPOCH() + END_EPOCH_WITH_LOCALS() { + if (state.get().pendingServiceFeePool > 0) + { + locals.distributeAuctionServiceFeeInput.feeAmount = state.get().pendingServiceFeePool; + CALL(DistributeAuctionServiceFee, locals.distributeAuctionServiceFeeInput, locals.distributeAuctionServiceFeeOutput); + state.mut().pendingServiceFeePool = 0; + } + state.mut().auctionList.cleanupIfNeeded(); } END_TICK_WITH_LOCALS() { makeDateStamp(qpi.year(), qpi.month(), qpi.day(), locals.currentDateStamp); + locals.currentDate = qpi.now(); + + if (!state.get().isEmergencyPaused) + { + locals.currentReserve = qpi.queryFeeReserve(SELF_INDEX); + if (!state.get().feeReserveBaselineAt.isValid()) + { + state.mut().feeReserveBaseline = locals.currentReserve; + state.mut().feeReserveBaselineAt = locals.currentDate; + } + else + { + 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); if (state.get().isAuctionTimerPaused) @@ -1890,7 +2104,6 @@ struct NOST : public ContractBase return; } - locals.currentDate = qpi.now(); locals.auctionIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); while (locals.auctionIndex != NULL_INDEX) { @@ -2004,6 +2217,12 @@ struct NOST : public ContractBase PRIVATE_FUNCTION(IsAuctionInteractionPaused) { + if (state.get().isEmergencyPaused) + { + output.isPaused = 1; + return; + } + output.isPaused = state.get().isAuctionTimerPaused; if (output.isPaused) { @@ -2016,6 +2235,22 @@ struct NOST : public ContractBase PRIVATE_PROCEDURE_WITH_LOCALS(SyncAuctionPauseState) { locals.currentDate = qpi.now(); + + 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); if (state.get().isPostBeginEpochPauseArmed) @@ -2483,6 +2718,15 @@ struct NOST : public ContractBase return; } + calculateBatchAuctionBidFee(input.effectiveQuantity, locals.auction.core.quantityForSale, input.bidAmount, locals.bidFeeCalculation); + // A full-auction-normalized bid value of zero cannot contribute to price discovery. + if (locals.bidFeeCalculation.fee == NOST_BATCH_BID_FEE_CUTOFF) + { + output.refundedAmount = static_cast(qpi.invocationReward()); + output.errorCode = EAuctionError::InvalidInput; + return; + } + if (input.bidAmount < locals.auction.core.salePrice) { output.refundedAmount = static_cast(qpi.invocationReward()); @@ -2506,8 +2750,7 @@ struct NOST : public ContractBase return; } - locals.requiredEscrow = smul(input.effectiveQuantity, input.bidAmount); - if (static_cast(qpi.invocationReward()) < locals.requiredEscrow) + if (static_cast(qpi.invocationReward()) < locals.bidFeeCalculation.requiredReward) { output.refundedAmount = static_cast(qpi.invocationReward()); output.errorCode = EAuctionError::InsufficientFunds; @@ -2533,7 +2776,7 @@ struct NOST : public ContractBase return; } - locals.participantData.escrowedAmount = locals.requiredEscrow; + locals.participantData.escrowedAmount = locals.bidFeeCalculation.escrowAmount; locals.participantData.requestedQuantity = input.effectiveQuantity; locals.participantData.allocatedQuantity = 0; locals.participantData.bidAmount = input.bidAmount; @@ -2632,13 +2875,19 @@ struct NOST : public ContractBase locals.recomputeBatchHighestBidInput.auctionIndex = input.auctionIndex; CALL(RecomputeBatchHighestBid, locals.recomputeBatchHighestBidInput, locals.recomputeBatchHighestBidOutput); - if (static_cast(qpi.invocationReward()) > locals.requiredEscrow) + if (locals.bidFeeCalculation.fee > 0) { - qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - locals.requiredEscrow); - output.refundedAmount = sadd(output.refundedAmount, static_cast(qpi.invocationReward()) - locals.requiredEscrow); + state.mut().pendingServiceFeePool = sadd(state.get().pendingServiceFeePool, locals.bidFeeCalculation.fee); } - output.escrowedAmount = locals.requiredEscrow; + if (static_cast(qpi.invocationReward()) > locals.bidFeeCalculation.requiredReward) + { + qpi.transfer(qpi.invocator(), static_cast(qpi.invocationReward()) - locals.bidFeeCalculation.requiredReward); + output.refundedAmount = + sadd(output.refundedAmount, static_cast(qpi.invocationReward()) - locals.bidFeeCalculation.requiredReward); + } + + output.escrowedAmount = locals.bidFeeCalculation.escrowAmount; output.success = 1; } @@ -3037,8 +3286,8 @@ struct NOST : public ContractBase 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; + locals.highestBidderExists = + locals.highestBidderData.isUsed && locals.highestBidderData.isActive && locals.highestBidderData.auctionIndex == input.auctionIndex; } if (locals.highestBidderExists && locals.highestBidderData.escrowedAmount > 0) @@ -3105,8 +3354,8 @@ struct NOST : public ContractBase 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; + locals.highestBidderExists = + locals.highestBidderData.isUsed && locals.highestBidderData.isActive && locals.highestBidderData.auctionIndex == input.auctionIndex; } if (locals.highestBidderExists && locals.highestBidderData.escrowedAmount > 0) @@ -3174,8 +3423,10 @@ struct NOST : public ContractBase * @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 Private auctions require the configured private auction fee, which is distributed between shareholders and the configured fee recipients, - * and must use exactly one access mode. + * @note A successful public Batch Auction accumulates the configured creation fee, distributed at `END_EPOCH`. Public Standard Auctions remain + * free, 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 exactly one access mode. */ PUBLIC_PROCEDURE_WITH_LOCALS(CreateAuction) { @@ -3346,7 +3597,8 @@ struct NOST : public ContractBase return; } - locals.requiredFee = getCreateAuctionFee(static_cast(input.auctionVisibility), state); + locals.requiredFee = + getCreateAuctionFee(static_cast(input.auctionType), static_cast(input.auctionVisibility), state); if (qpi.invocationReward() < locals.requiredFee) { if (qpi.invocationReward() > 0) @@ -3442,8 +3694,10 @@ struct NOST : public ContractBase return; } - locals.distributeAuctionServiceFeeInput.feeAmount = static_cast(locals.requiredFee); - CALL(DistributeAuctionServiceFee, locals.distributeAuctionServiceFeeInput, locals.distributeAuctionServiceFeeOutput); + if (locals.requiredFee > 0) + { + state.mut().pendingServiceFeePool = sadd(state.get().pendingServiceFeePool, static_cast(locals.requiredFee)); + } if (qpi.invocationReward() > locals.requiredFee) { @@ -3461,6 +3715,9 @@ struct NOST : public ContractBase * @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(101 - floor(quantity * bidAmount / quantityForSale), 0)` qu for distribution at `END_EPOCH`. A zero normalized value is rejected. 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`. @@ -3633,6 +3890,19 @@ struct NOST : public ContractBase output.cancellationFee = 0; output.errorCode = EAuctionError::InvalidInput; + if (state.get().isEmergencyPaused) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::AuctionPaused; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); + logProcedureResult(locals.log); + return; + } + if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { if (qpi.invocationReward() > 0) @@ -3854,10 +4124,11 @@ struct NOST : public ContractBase return; } - if (!isValidAuctionFeeConfiguration( - input.privateAuctionFee, input.auctionCancellationFeeBasisPoints, input.managementFeeBasisPoints, input.developmentFeeBasisPoints, - input.takeoverCoordinatorFeeBasisPoints, input.shareholderDividendBasisPoints, input.shareholderFeeBasisPointsTier1, - input.shareholderFeeBasisPointsTier2, input.shareholderFeeBasisPointsTier3, input.shareholderFeeBasisPointsTier4)) + if (!isValidAuctionFeeConfiguration(input.privateAuctionFee, input.batchAuctionCreationFee, 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); @@ -3866,6 +4137,7 @@ struct NOST : public ContractBase } state.mut().privateAuctionFee = input.privateAuctionFee; + state.mut().batchAuctionCreationFee = input.batchAuctionCreationFee; state.mut().auctionCancellationFeeBasisPoints = input.auctionCancellationFeeBasisPoints; state.mut().managementFeeBasisPoints = input.managementFeeBasisPoints; state.mut().developmentFeeBasisPoints = input.developmentFeeBasisPoints; @@ -3901,10 +4173,11 @@ struct NOST : public ContractBase return; } - if (!isValidAuctionFeeConfiguration( - input.privateAuctionFee, input.auctionCancellationFeeBasisPoints, input.managementFeeBasisPoints, input.developmentFeeBasisPoints, - state.get().takeoverCoordinatorFeeBasisPoints, state.get().shareholderDividendBasisPoints, input.shareholderFeeBasisPointsTier1, - input.shareholderFeeBasisPointsTier2, input.shareholderFeeBasisPointsTier3, input.shareholderFeeBasisPointsTier4)) + if (!isValidAuctionFeeConfiguration(input.privateAuctionFee, input.batchAuctionCreationFee, 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); @@ -3913,6 +4186,7 @@ struct NOST : public ContractBase } state.mut().privateAuctionFee = input.privateAuctionFee; + state.mut().batchAuctionCreationFee = input.batchAuctionCreationFee; state.mut().auctionCancellationFeeBasisPoints = input.auctionCancellationFeeBasisPoints; state.mut().managementFeeBasisPoints = input.managementFeeBasisPoints; state.mut().developmentFeeBasisPoints = input.developmentFeeBasisPoints; @@ -3960,6 +4234,80 @@ struct NOST : public ContractBase 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; + 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; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state.get().takeoverCoordinator && qpi.invocator() != state.get().management) + { + output.errorCode = EAuctionError::Forbidden; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetEmergencyPause, output.errorCode, 0, 0); + logProcedureResult(locals.log); + return; + } + + if (input.paused) + { + state.mut().isEmergencyPaused = 1; + state.mut().emergencyPausedAt = qpi.now(); + } + else + { + state.mut().isEmergencyPaused = 0; + state.mut().emergencyPausedAt.setInvalid(); + state.mut().feeReserveBaselineAt.setInvalid(); + } + + output.errorCode = EAuctionError::Success; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::SetEmergencyPause, output.errorCode, 0, 0); + logProcedureResult(locals.log); + } + /** * @brief Returns the stored state of one auction. * @note The response contains a serializable auction view; access-control containers are returned as fixed arrays with counts. @@ -4060,8 +4408,18 @@ struct NOST : public ContractBase output.shareholderFeeBasisPointsTier2 = state.get().shareholderFeeBasisPointsTier2; output.shareholderFeeBasisPointsTier3 = state.get().shareholderFeeBasisPointsTier3; output.shareholderFeeBasisPointsTier4 = state.get().shareholderFeeBasisPointsTier4; + output.batchAuctionCreationFee = state.get().batchAuctionCreationFee; } + /** + * @brief Calculates the escrow, accumulated fee, and reward required by Batch Auction bid arithmetic. + * @param input Prospective bid quantity, total auction quantity, and price per asset; zero values are accepted for arithmetic inspection. + * @param output Saturating escrow product, tapered bid fee, and saturating total reward. + * @note Division by `tokensForSale` rounds down; when it is zero, the normalized bid value is treated as zero. + * @note This function does not validate whether `PlaceBid` would accept the bid or mutate contract state. + */ + PUBLIC_FUNCTION(CalculateBatchAuctionBidFee) { calculateBatchAuctionBidFee(input.bidQuantity, input.tokensForSale, input.bidAmount, output); } + /** * @brief Returns the current wallets that receive auction fee transfers. * @note The response exposes the configured management, development, and takeover coordinator addresses. @@ -4089,15 +4447,36 @@ struct NOST : public ContractBase */ PUBLIC_FUNCTION(GetRouteAllFeesToDevelopment) { output.enabled = state.get().routeAllFeesToDevelopment; } + /** + * @brief Returns the amount of accumulated auction service fees awaiting distribution at `END_EPOCH`. + */ + PUBLIC_FUNCTION(GetPendingServiceFeePool) { output.pendingServiceFeePool = state.get().pendingServiceFeePool; } + + /** + * @brief Returns the current state of the execution fee reserve guard, including a live reserve reading. + */ + PUBLIC_FUNCTION(GetFeeReserveGuardState) + { + output.currentFeeReserve = qpi.queryFeeReserve(SELF_INDEX); + output.feeReserveBaseline = state.get().feeReserveBaseline; + output.feeReserveBaselineAt = state.get().feeReserveBaselineAt; + output.emergencyPausedAt = state.get().emergencyPausedAt; + output.dropBasisPoints = state.get().feeReserveGuardDropBasisPoints; + output.windowSeconds = state.get().feeReserveGuardWindowSeconds; + output.isEmergencyPaused = state.get().isEmergencyPaused; + } + 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 = state.get().pendingServiceFeePool; 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; for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) { @@ -4344,6 +4723,19 @@ struct NOST : public ContractBase output.transferredNumberOfShares = 0; output.errorCode = EAuctionError::InvalidInput; + 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; + } + if (input.numberOfShares > 0 && qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) >= input.numberOfShares) { @@ -4472,8 +4864,7 @@ struct NOST : public ContractBase { quantityForSale = 0; resolvedMinimumPurchaseQuantity = 0; - if (initialPrice < NOST_STANDARD_MIN_PRICE || salePrice < NOST_STANDARD_MIN_PRICE || - minimumBidIncrement < NOST_STANDARD_MIN_BID_INCREMENT) + if (initialPrice < NOST_STANDARD_MIN_PRICE || salePrice < NOST_STANDARD_MIN_PRICE || minimumBidIncrement < NOST_STANDARD_MIN_BID_INCREMENT) { return false; } @@ -4499,13 +4890,14 @@ struct NOST : public ContractBase return visibility != EAuctionVisibility::Private || ((requiredAccessAssetCount > 0) != (allowedWalletCount > 0)); } - constexpr static bool isValidAuctionFeeConfiguration(sint64 privateAuctionFee, uint64 auctionCancellationFeeBasisPoints, - uint64 managementFeeBasisPoints, uint64 developmentFeeBasisPoints, - uint64 takeoverCoordinatorFeeBasisPoints, uint64 shareholderDividendBasisPoints, - uint64 shareholderFeeBasisPointsTier1, uint64 shareholderFeeBasisPointsTier2, - uint64 shareholderFeeBasisPointsTier3, uint64 shareholderFeeBasisPointsTier4) + constexpr static bool isValidAuctionFeeConfiguration(sint64 privateAuctionFee, uint64 batchAuctionCreationFee, + uint64 auctionCancellationFeeBasisPoints, uint64 managementFeeBasisPoints, + uint64 developmentFeeBasisPoints, uint64 takeoverCoordinatorFeeBasisPoints, + uint64 shareholderDividendBasisPoints, uint64 shareholderFeeBasisPointsTier1, + uint64 shareholderFeeBasisPointsTier2, uint64 shareholderFeeBasisPointsTier3, + uint64 shareholderFeeBasisPointsTier4) { - return privateAuctionFee >= 0 && auctionCancellationFeeBasisPoints <= NOST_BASIS_POINTS_SCALE && + return privateAuctionFee >= 0 && batchAuctionCreationFee <= UINT64_MAX && 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 && @@ -4578,9 +4970,25 @@ struct NOST : public ContractBase output.developmentFeeAmount - output.takeoverCoordinatorFeeAmount); } - static sint64 getCreateAuctionFee(EAuctionVisibility visibility, const ContractState& state) + static void calculateBatchAuctionBidFee(uint64 bidQuantity, uint64 tokensForSale, uint64 bidAmount, CalculateBatchAuctionBidFee_output& output) + { + output.escrowAmount = smul(bidQuantity, bidAmount); + output.fee = tokensForSale == 0 ? NOST_BATCH_BID_FEE_CUTOFF + : div(output.escrowAmount, tokensForSale) < NOST_BATCH_BID_FEE_CUTOFF + ? NOST_BATCH_BID_FEE_CUTOFF - div(output.escrowAmount, tokensForSale) + : 0; + output.requiredReward = sadd(output.escrowAmount, output.fee); + } + + static sint64 getCreateAuctionFee(EAuctionType auctionType, EAuctionVisibility visibility, const ContractState& state) { - return visibility == EAuctionVisibility::Private ? state.get().privateAuctionFee : 0; + if (visibility == EAuctionVisibility::Private) + { + return state.get().privateAuctionFee; + } + return auctionType == EAuctionType::Batch && visibility == EAuctionVisibility::Public + ? static_cast(state.get().batchAuctionCreationFee) + : 0; } static bool isSupportedAuctionType(EAuctionType auctionType) diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index 87391a4fd..cb0733c50 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -95,6 +95,8 @@ class ContractTestingNOST : protected ContractTesting 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{}; @@ -138,9 +140,9 @@ class ContractTestingNOST : protected ContractTesting return output.transferredNumberOfShares; } - NOST::CreateAuction_output createAuction(const id& seller, const NOST::CreateAuction_input& input, sint64 reward = 0) + NOST::CreateAuction_output createAuction(const id& seller, const NOST::CreateAuction_input& input, + sint64 reward = NOST_PUBLIC_BATCH_AUCTION_CREATION_FEE) { - NOST::CreateAuction_output output{}; if (reward > 0) { seedUser(seller, reward); @@ -149,6 +151,12 @@ class ContractTestingNOST : protected ContractTesting { 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; } @@ -180,6 +188,20 @@ class ContractTestingNOST : protected ContractTesting return output; } + NOST::PlaceBid_output placeBatchBidWithRequiredReward(const id& bidder, uint64 auctionIndex, uint64 bidQuantity, uint64 bidAmount) + { + const auto auction = getAuction(auctionIndex); + const auto calculation = calculateBatchAuctionBidFee(bidQuantity, auction.auction.core.quantityForSale, 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 auto auction = getAuction(auctionIndex); + const auto calculation = calculateBatchAuctionBidFee(bidQuantity, auction.auction.core.quantityForSale, 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{}; @@ -311,6 +333,52 @@ class ContractTestingNOST : protected ContractTesting return output; } + NOST::SetAuctionFees_input makeCoordinatorFeeInput(uint64 batchAuctionCreationFee) const + { + const auto fees = getAuctionFees(); + NOST::SetAuctionFees_input input{}; + input.privateAuctionFee = fees.privateAuctionFee; + input.batchAuctionCreationFee = batchAuctionCreationFee; + 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(uint64 batchAuctionCreationFee) const + { + const auto fees = getAuctionFees(); + NOST::SetAuctionFeesByManagement_input input{}; + input.privateAuctionFee = fees.privateAuctionFee; + input.batchAuctionCreationFee = batchAuctionCreationFee; + 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 tokensForSale, uint64 bidAmount) const + { + NOST::CalculateBatchAuctionBidFee_input input{}; + NOST::CalculateBatchAuctionBidFee_output output{}; + + input.bidQuantity = bidQuantity; + input.tokensForSale = tokensForSale; + input.bidAmount = bidAmount; + callFunction(NOST_CONTRACT_INDEX, 20, input, output); + return output; + } + NOST::GetFeeRecipients_output getFeeRecipients() const { NOST::GetFeeRecipients_input input{}; @@ -466,6 +534,47 @@ class ContractTestingNOST : protected ContractTesting 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::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]); } @@ -675,6 +784,7 @@ TEST(ContractNostromoAuction, InitialStateAndGettersAuction) const auto fees = nostromo.getAuctionFees(); EXPECT_EQ(fees.privateAuctionFee, NOST_DEFAULT_PRIVATE_AUCTION_FEE); + EXPECT_EQ(fees.batchAuctionCreationFee, static_cast(NOST_PUBLIC_BATCH_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); @@ -789,8 +899,8 @@ TEST(ContractNostromoAuction, AuctionIndexAndExpandedGetterSurfaceAuction) EXPECT_EQ(batch.auctions.get(0).auctionIndex, 2ULL); EXPECT_EQ(batch.auctions.get(2).auctionIndex, 0ULL); - ASSERT_EQ(nostromo.placeBid(bidderA, createA.auctionIndex, 2, 11, 22).errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(bidderB, createA.auctionIndex, 1, 15, 15).errorCode, NOST::EAuctionError::Success); + 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); @@ -962,6 +1072,192 @@ TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 9); } +TEST(ContractNostromoAuction, PublicBatchCreationAccumulatesConfiguredFeeAndRefundsExcessAuction) +{ + ContractTestingNOST nostromo; + const id seller(901, 902, 903, 904); + const Asset asset{seller, assetNameFromString("BCRFEE")}; + constexpr uint64 configuredFee = 73; + const auto feeInput = nostromo.makeCoordinatorFeeInput(configuredFee); + ASSERT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), feeInput).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.getAuctionFees().batchAuctionCreationFee, 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 - static_cast(configuredFee)); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + static_cast(configuredFee)); + expectedPool += 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 - static_cast(2 * configuredFee)); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + static_cast(2 * configuredFee)); + expectedPool += configuredFee; + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, expectedPool); + + constexpr uint64 managementConfiguredFee = 29; + const auto managementFeeInput = nostromo.makeManagementFeeInput(managementConfiguredFee); + ASSERT_EQ(nostromo.setAuctionFeesByManagement(ContractTestingNOST::managementWallet(), managementFeeInput).errorCode, + NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.getAuctionFees().batchAuctionCreationFee, managementConfiguredFee); + const auto managementConfigured = nostromo.createAuctionWithFundedReward(seller, input, managementConfiguredFee); + ASSERT_EQ(managementConfigured.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(seller), sellerBefore - static_cast(2 * configuredFee + managementConfiguredFee)); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + static_cast(2 * configuredFee + managementConfiguredFee)); + expectedPool += 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, 1), 1); + ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(standardSeller, standardAsset, 1), 1); + const auto standardInput = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(standardAsset, 1)); + EXPECT_EQ(nostromo.createAuctionWithFundedReward(standardSeller, standardInput, 0).errorCode, NOST::EAuctionError::Success); + 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); + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, 0ULL); +} + +TEST(ContractNostromoAuction, BatchBidFeeBoundariesAuction) +{ + ContractTestingNOST nostromo; + const struct + { + uint64 bidQuantity; + uint64 tokensForSale; + uint64 bidAmount; + uint64 escrowAmount; + uint64 fee; + uint64 requiredReward; + } cases[] = { + {1, 10, 9, 9, 101, 110}, + {1, 10, 10, 10, 100, 110}, + {1, 10, 20, 20, 99, 119}, + {1, 10, 30, 30, 98, 128}, + {10, 10, 100, 1000, 1, 1001}, + {1, 1, 101, 101, 0, 101}, + {2, 1, 101, 202, 0, 202}, + {2, 10, 19, 38, 98, 136}, + {2, 0, 100, 200, 101, 301}, + {UINT64_MAX, 1, 2, UINT64_MAX, 0, UINT64_MAX}, + }; + + for (const auto& testCase : cases) + { + const auto output = + nostromo.calculateBatchAuctionBidFee(testCase.bidQuantity, testCase.tokensForSale, testCase.bidAmount); + EXPECT_EQ(output.escrowAmount, testCase.escrowAmount); + EXPECT_EQ(output.fee, testCase.fee); + EXPECT_EQ(output.requiredReward, testCase.requiredReward); + } +} + +TEST(ContractNostromoAuction, BatchAuctionCreationFeeConfigurationBoundariesAuction) +{ + ContractTestingNOST nostromo; + EXPECT_EQ(nostromo.getAuctionFees().batchAuctionCreationFee, static_cast(NOST_PUBLIC_BATCH_AUCTION_CREATION_FEE)); + + auto coordinatorInput = nostromo.makeCoordinatorFeeInput(0); + ASSERT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getAuctionFees().batchAuctionCreationFee, 0ULL); + 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.batchAuctionCreationFee = static_cast(INT64_MAX); + ASSERT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getAuctionFees().batchAuctionCreationFee, static_cast(INT64_MAX)); + + auto managementInput = nostromo.makeManagementFeeInput(41); + ASSERT_EQ(nostromo.setAuctionFeesByManagement(ContractTestingNOST::managementWallet(), managementInput).errorCode, + NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.getAuctionFees().batchAuctionCreationFee, 41ULL); +} + +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 zeroNormalizedBid = nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 1, 9, 110); + EXPECT_EQ(zeroNormalizedBid.errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(zeroNormalizedBid.refundedAmount, 110ULL); + EXPECT_EQ(getBalance(firstBidder), firstBidderBefore); + + const auto calculation = nostromo.calculateBatchAuctionBidFee(2, 10, 100); + ASSERT_EQ(calculation.escrowAmount, 200ULL); + ASSERT_EQ(calculation.fee, 81ULL); + ASSERT_EQ(calculation.requiredReward, 281ULL); + const auto underfunded = + nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 2, 100, calculation.requiredReward - 1); + EXPECT_EQ(underfunded.errorCode, NOST::EAuctionError::InsufficientFunds); + EXPECT_EQ(underfunded.refundedAmount, 280ULL); + EXPECT_EQ(getBalance(firstBidder), firstBidderBefore); + + const auto accepted = + nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 2, 100, calculation.requiredReward + 49); + ASSERT_EQ(accepted.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(accepted.escrowedAmount, 200ULL); + EXPECT_EQ(accepted.refundedAmount, 49ULL); + EXPECT_EQ(getBalance(firstBidder), firstBidderBefore - 281); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + 281); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, poolBefore + 81ULL); + + // 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 + 200 - static_cast(poolBefore)); +} + TEST(ContractNostromoAuction, CreateStandardSingleAssetAuctionEscrowsLotAuction) { ContractTestingNOST nostromo; @@ -975,8 +1271,10 @@ TEST(ContractNostromoAuction, CreateStandardSingleAssetAuctionEscrowsLotAuction) auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 5)); input.minimumPurchaseQuantity = UINT64_MAX; - const auto output = nostromo.createAuction(seller, input); + const sint64 sellerBalanceBefore = getBalance(seller); + const auto output = nostromo.createAuctionWithFundedReward(seller, input, 0); ASSERT_EQ(output.errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(getBalance(seller), sellerBalanceBefore); const auto auction = nostromo.getAuction(output.auctionIndex).auction; EXPECT_EQ(auction.core.quantityForSale, 1ULL); @@ -1041,7 +1339,7 @@ TEST(ContractNostromoAuction, BatchBidEnforcesMinimumPurchaseQuantityAndRefundsA EXPECT_EQ(rejectedBid.escrowedAmount, 0ULL); EXPECT_EQ(getBalance(bidder), balanceBeforeRejectedBid); - const auto acceptedBid = nostromo.placeBidWithFundedReward(bidder, createOutput.auctionIndex, 10, 10, 100); + const auto acceptedBid = nostromo.placeBatchBidWithFundedRequiredReward(bidder, createOutput.auctionIndex, 10, 10); EXPECT_EQ(acceptedBid.errorCode, NOST::EAuctionError::Success); EXPECT_EQ(acceptedBid.escrowedAmount, 100ULL); } @@ -1311,7 +1609,8 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacit 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.placeBid(allowedBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(allowedBidder, createOutput.auctionIndex, 1, 10).errorCode, + NOST::EAuctionError::Success); } { @@ -1344,7 +1643,8 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacit 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.placeBid(accessBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(accessBidder, createOutput.auctionIndex, 1, 10).errorCode, + NOST::EAuctionError::Success); } } @@ -1383,6 +1683,17 @@ TEST(ContractNostromoAuction, PrivateAuctionFeeIsDistributedAcrossRecipientsAuct 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)); + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, 0ULL); + if (routeMode != 0) { EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); @@ -1670,7 +1981,7 @@ TEST(ContractNostromoAuction, PlaceBidRejectsWhenParticipantStorageIsFullAuction } ASSERT_EQ(usedParticipantCount, NOST_AUCTION_PARTICIPANT_NUM); - const auto output = nostromo.placeBid(bidder, createOutput.auctionIndex, 1, 10, 10); + 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); @@ -1710,8 +2021,8 @@ TEST(ContractNostromoAuction, PlaceBatchBidValidatesAndRecomputesHighestBidAucti const auto insufficientFunds = nostromo.placeBid(bidderA, createOutput.auctionIndex, 2, 12, 23); EXPECT_EQ(insufficientFunds.errorCode, NOST::EAuctionError::InsufficientFunds); - const auto bidA1 = nostromo.placeBid(bidderA, createOutput.auctionIndex, 2, 20, 40); - const auto bidB = nostromo.placeBid(bidderB, createOutput.auctionIndex, 3, 15, 45); + 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); @@ -1754,7 +2065,7 @@ TEST(ContractNostromoAuction, PlaceBatchBidExtendsAuctionNearEndAuction) ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); nostromo.setNow(2026, 1, 2, 8, 56, 30); - const auto bidOutput = nostromo.placeBid(bidder, createOutput.auctionIndex, 1, 15, 15); + 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; @@ -1774,19 +2085,19 @@ TEST(ContractNostromoAuction, BatchBidAvailabilityRejectsOversizedTailAuction) const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2)); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(bidderA, createOutput.auctionIndex, 8, 4, 32).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, 2, 6); + 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.placeBid(bidderB, createOutput.auctionIndex, 2, 2, 4); + 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); } @@ -1804,14 +2115,14 @@ TEST(ContractNostromoAuction, BatchCoveredLotRequiresHigherPriceAuction) const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 10, 2)); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(bidderA, createOutput.auctionIndex, 10, 3, 30).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, 4ULL); + EXPECT_EQ(availability.minimumBidPrice, 31ULL); EXPECT_EQ(availability.availableQuantity, 0ULL); - EXPECT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, 2, 2).errorCode, NOST::EAuctionError::BidTooLow); - EXPECT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 1, 3, 3).errorCode, NOST::EAuctionError::BidTooLow); + 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.placeBid(bidderB, createOutput.auctionIndex, 3, 4, 12).errorCode, NOST::EAuctionError::Success); + 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); @@ -1837,13 +2148,15 @@ TEST(ContractNostromoAuction, BatchMinimumPurchaseTailAndSameBidderDisplacementA const auto createOutput = nostromo.createAuction(seller, input); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(bidderA, createOutput.auctionIndex, 8, 4, 32).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, 5ULL); + 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, 4, 12).errorCode, NOST::EAuctionError::BidTooLow); - ASSERT_EQ(nostromo.placeBid(bidderB, createOutput.auctionIndex, 3, 5, 15).errorCode, NOST::EAuctionError::Success); + 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); @@ -1864,28 +2177,29 @@ TEST(ContractNostromoAuction, BatchMinimumPurchaseTailAndSameBidderDisplacementA const auto createOutput = nostromo.createAuction(seller, input); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(bidderA, createOutput.auctionIndex, 10, 3, 30).errorCode, NOST::EAuctionError::Success); - const auto improved = nostromo.placeBid(bidderA, createOutput.auctionIndex, 8, 4, 32); + 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, 24ULL); + EXPECT_EQ(improved.refundedAmount, 240ULL); const auto participants = nostromo.getAuctionParticipants(createOutput.auctionIndex, 0, 64); ASSERT_EQ(participants.totalCount, 2ULL); - uint64 quantityAtThree = 0; - uint64 quantityAtFour = 0; + uint64 quantityAtThirty = 0; + uint64 quantityAtForty = 0; for (uint64 index = 0; index < participants.returnedCount; ++index) { - if (participants.participants.get(index).bidAmount == 3) + if (participants.participants.get(index).bidAmount == 30) { - quantityAtThree = participants.participants.get(index).requestedQuantity; + quantityAtThirty = participants.participants.get(index).requestedQuantity; } - if (participants.participants.get(index).bidAmount == 4) + if (participants.participants.get(index).bidAmount == 40) { - quantityAtFour = participants.participants.get(index).requestedQuantity; + quantityAtForty = participants.participants.get(index).requestedQuantity; } } - EXPECT_EQ(quantityAtThree, 2ULL); - EXPECT_EQ(quantityAtFour, 8ULL); + EXPECT_EQ(quantityAtThirty, 2ULL); + EXPECT_EQ(quantityAtForty, 8ULL); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); EXPECT_EQ(nostromo.managedShares(asset, bidderA), 10); @@ -1917,7 +2231,8 @@ TEST(ContractNostromoAuction, BatchAvailabilityGetterStatesAuction) 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.placeBid(bidder, closedBatchCreate.auctionIndex, 1, 2, 2).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); @@ -2036,7 +2351,8 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); EXPECT_EQ(nostromo.placeBid(denied, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::PrivateAuctionAccessDenied); - EXPECT_EQ(nostromo.placeBid(allowed, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::Success); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(allowed, createOutput.auctionIndex, 1, 12).errorCode, + NOST::EAuctionError::Success); } { @@ -2070,8 +2386,10 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) EXPECT_EQ(nostromo.placeBid(belowThresholdBidder, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::PrivateAuctionAccessDenied); - EXPECT_EQ(nostromo.placeBid(exactThresholdBidder, createOutput.auctionIndex, 1, 12, 12).errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.placeBid(alternateAssetBidder, createOutput.auctionIndex, 1, 13, 13).errorCode, NOST::EAuctionError::Success); + 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); } } @@ -2128,11 +2446,14 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 4, 10)); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(bidderA, createOutput.auctionIndex, 3, 15, 45).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.placeBid(bidderB, createOutput.auctionIndex, 1, 15, 45).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderB, createOutput.auctionIndex, 1, 15).errorCode, + NOST::EAuctionError::Success); nostromo.setNow(2026, 1, 1, 9, 0, 2); - ASSERT_EQ(nostromo.placeBid(bidderC, createOutput.auctionIndex, 2, 20, 40).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderC, createOutput.auctionIndex, 2, 20).errorCode, + NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); @@ -2173,7 +2494,8 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 5, 10)); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 2, 12, 24).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); @@ -2200,11 +2522,11 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF NOST::AuctionRevenueBreakdown expectedRevenue{}; nostromo.calculateAuctionRevenueBreakdown(200ULL, expectedRevenue); const sint64 sellerBalanceBefore = getBalance(seller); - nostromo.seedUser(firstBidder, 200); + nostromo.seedUser(firstBidder, 291); nostromo.seedUser(secondBidder, 150); const sint64 secondBidderBalanceBefore = getBalance(secondBidder); - ASSERT_EQ(nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 10, 20, 200).errorCode, + 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); @@ -2238,8 +2560,9 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF const auto createOutput = nostromo.createAuction(seller, input); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(firstBidder, createOutput.auctionIndex, 10, 20, 200).errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(partialBidder, createOutput.auctionIndex, 15, 15, 225).errorCode, + 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); @@ -2267,18 +2590,21 @@ TEST(ContractNostromoAuction, BatchFinalizationRefundsLosingBidsAndTieBreaksByBi 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); + nostromo.seedUser(earlierBidder, 106); + nostromo.seedUser(laterBidder, 106); + nostromo.seedUser(higherBidder, 107); const sint64 earlierBefore = getBalance(earlierBidder); const sint64 laterBefore = getBalance(laterBidder); const sint64 higherBefore = getBalance(higherBidder); - ASSERT_EQ(nostromo.placeBidWithFundedReward(earlierBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithFundedRequiredReward(earlierBidder, createOutput.auctionIndex, 1, 10).errorCode, + NOST::EAuctionError::Success); nostromo.setNow(2026, 1, 1, 9, 0, 1); - ASSERT_EQ(nostromo.placeBidWithFundedReward(laterBidder, createOutput.auctionIndex, 1, 10, 10).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithFundedRequiredReward(laterBidder, createOutput.auctionIndex, 1, 10).errorCode, + NOST::EAuctionError::Success); nostromo.setNow(2026, 1, 1, 9, 0, 2); - ASSERT_EQ(nostromo.placeBidWithFundedReward(higherBidder, createOutput.auctionIndex, 1, 11, 11).errorCode, NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.placeBatchBidWithFundedRequiredReward(higherBidder, createOutput.auctionIndex, 1, 11).errorCode, + NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); @@ -2297,9 +2623,9 @@ TEST(ContractNostromoAuction, BatchFinalizationRefundsLosingBidsAndTieBreaksByBi 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 - 10); - EXPECT_EQ(getBalance(laterBidder), laterBefore); - EXPECT_EQ(getBalance(higherBidder), higherBefore - 11); + EXPECT_EQ(getBalance(earlierBidder), earlierBefore - 106); + EXPECT_EQ(getBalance(laterBidder), laterBefore - 96); + EXPECT_EQ(getBalance(higherBidder), higherBefore - 107); } TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) @@ -2806,7 +3132,8 @@ TEST(ContractNostromoAuction, CancelAuctionRefundsBidsAndFreesParticipantSlotsAu const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 10)); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, 12, 12).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); @@ -2875,7 +3202,8 @@ TEST(ContractNostromoAuction, ClosedAuctionHistoryRecordsFinalizedAndCancelledAu 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.placeBid(bidder, finalizedCreateOutput.auctionIndex, 1, 10, 10).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); @@ -2923,6 +3251,7 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) NOST::SetAuctionFees_input coordinatorInput{}; coordinatorInput.privateAuctionFee = 60000000; + coordinatorInput.batchAuctionCreationFee = 123; coordinatorInput.auctionCancellationFeeBasisPoints = 900; coordinatorInput.managementFeeBasisPoints = 60; coordinatorInput.developmentFeeBasisPoints = 70; @@ -2952,6 +3281,7 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) EXPECT_EQ(fees.takeoverCoordinatorFeeBasisPoints, 80ULL); EXPECT_EQ(fees.shareholderDividendBasisPoints, 8500ULL); EXPECT_EQ(fees.shareholderFeeBasisPointsTier1, 400ULL); + EXPECT_EQ(fees.batchAuctionCreationFee, 123ULL); const auto setManagementForbidden = nostromo.setManagement(outsider, newManagement); EXPECT_EQ(setManagementForbidden.errorCode, NOST::EAuctionError::Forbidden); @@ -2965,6 +3295,7 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) NOST::SetAuctionFeesByManagement_input managementInput{}; managementInput.privateAuctionFee = 70000000; + managementInput.batchAuctionCreationFee = 456; managementInput.auctionCancellationFeeBasisPoints = 800; managementInput.managementFeeBasisPoints = 90; managementInput.developmentFeeBasisPoints = 110; @@ -2987,6 +3318,7 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) fees = nostromo.getAuctionFees(); EXPECT_EQ(fees.privateAuctionFee, 70000000); + EXPECT_EQ(fees.batchAuctionCreationFee, 456ULL); EXPECT_EQ(fees.auctionCancellationFeeBasisPoints, 800ULL); EXPECT_EQ(fees.managementFeeBasisPoints, 90ULL); EXPECT_EQ(fees.developmentFeeBasisPoints, 110ULL); @@ -3070,3 +3402,133 @@ TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) } } } + +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); + + 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); + + EXPECT_EQ(nostromo.setFeeReserveGuardConfig(ContractTestingNOST::takeoverCoordinatorWallet(), 500ULL, 300ULL).errorCode, + NOST::EAuctionError::Success); + auto 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 auto 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); + + // The auction should remain paused after END_EPOCH; only a manual resume clears it. + EXPECT_EQ(nostromo.getFeeReserveGuardState().isEmergencyPaused, 1); +} From 568fc362617f15c66a3ac3856897938c3d8d261c Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 9 Jul 2026 20:46:09 +0300 Subject: [PATCH 50/59] Refactor batch auction bid fee calculation; remove unused tokensForSale parameter and adjust fee logic --- src/contracts/Nostromo.h | 43 ++++--- test/contract_nostromo.cpp | 227 +++++++++++++++---------------------- 2 files changed, 112 insertions(+), 158 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 9e7db8bdf..ae7eff1f5 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -34,8 +34,8 @@ constexpr uint32 NOST_AUCTION_MAX_DURATION_DAYS = 30; constexpr sint64 NOST_DEFAULT_PRIVATE_AUCTION_FEE = 50000000LL; // Default fee accumulated after successfully creating a public Batch Auction and distributed at END_EPOCH, in qu. constexpr uint64 NOST_PUBLIC_BATCH_AUCTION_CREATION_FEE = 100LL; -// Exclusive upper bound used to taper the accumulated Batch Auction bid fee against its normalized bid value, in qu. -constexpr uint64 NOST_BATCH_BID_FEE_CUTOFF = 101ULL; +// 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. @@ -820,9 +820,6 @@ struct NOST : public ContractBase /** @brief Number of assets requested by the prospective bid. */ uint64 bidQuantity; - /** @brief Total number of assets offered by the Batch Auction. */ - uint64 tokensForSale; - /** @brief Prospective price per asset, in qu. */ uint64 bidAmount; }; @@ -833,8 +830,7 @@ struct NOST : public ContractBase /** @brief Saturating product of `bidQuantity` and `bidAmount`. */ uint64 escrowAmount; - /** @brief Amount accumulated for distribution at `END_EPOCH` for an accepted bid: `max(101 - bidQuantity * floor(bidAmount / tokensForSale), - * 0)`. */ + /** @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`. */ @@ -2718,9 +2714,8 @@ struct NOST : public ContractBase return; } - calculateBatchAuctionBidFee(input.effectiveQuantity, locals.auction.core.quantityForSale, input.bidAmount, locals.bidFeeCalculation); - // A full-auction-normalized bid value of zero cannot contribute to price discovery. - if (locals.bidFeeCalculation.fee == NOST_BATCH_BID_FEE_CUTOFF) + calculateBatchAuctionBidFee(input.effectiveQuantity, input.bidAmount, locals.bidFeeCalculation); + if (locals.bidFeeCalculation.escrowAmount == 0) { output.refundedAmount = static_cast(qpi.invocationReward()); output.errorCode = EAuctionError::InvalidInput; @@ -3715,9 +3710,9 @@ struct NOST : public ContractBase * @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(101 - floor(quantity * bidAmount / quantityForSale), 0)` qu for distribution at `END_EPOCH`. A zero normalized value is rejected. Excess - * reward is refunded; rejected bids refund the full reward. The accumulated fee is not refunded if the bid is later displaced. + * @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`. @@ -4413,12 +4408,12 @@ struct NOST : public ContractBase /** * @brief Calculates the escrow, accumulated fee, and reward required by Batch Auction bid arithmetic. - * @param input Prospective bid quantity, total auction quantity, and price per asset; zero values are accepted for arithmetic inspection. - * @param output Saturating escrow product, tapered bid fee, and saturating total reward. - * @note Division by `tokensForSale` rounds down; when it is zero, the normalized bid value is treated as zero. + * @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.tokensForSale, input.bidAmount, output); } + PUBLIC_FUNCTION(CalculateBatchAuctionBidFee) { calculateBatchAuctionBidFee(input.bidQuantity, input.bidAmount, output); } /** * @brief Returns the current wallets that receive auction fee transfers. @@ -4970,13 +4965,17 @@ struct NOST : public ContractBase output.developmentFeeAmount - output.takeoverCoordinatorFeeAmount); } - static void calculateBatchAuctionBidFee(uint64 bidQuantity, uint64 tokensForSale, uint64 bidAmount, CalculateBatchAuctionBidFee_output& output) + static void calculateBatchAuctionBidFee(uint64 bidQuantity, uint64 bidAmount, CalculateBatchAuctionBidFee_output& output) { output.escrowAmount = smul(bidQuantity, bidAmount); - output.fee = tokensForSale == 0 ? NOST_BATCH_BID_FEE_CUTOFF - : div(output.escrowAmount, tokensForSale) < NOST_BATCH_BID_FEE_CUTOFF - ? NOST_BATCH_BID_FEE_CUTOFF - div(output.escrowAmount, tokensForSale) - : 0; + if (output.escrowAmount == 0) + { + output.fee = 0; + output.requiredReward = 0; + return; + } + + output.fee = output.escrowAmount <= NOST_BATCH_BID_FEE_CUTOFF ? NOST_BATCH_BID_FEE_CUTOFF - output.escrowAmount : 0; output.requiredReward = sadd(output.escrowAmount, output.fee); } diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index cb0733c50..ee3e77c0b 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -190,15 +190,13 @@ class ContractTestingNOST : protected ContractTesting NOST::PlaceBid_output placeBatchBidWithRequiredReward(const id& bidder, uint64 auctionIndex, uint64 bidQuantity, uint64 bidAmount) { - const auto auction = getAuction(auctionIndex); - const auto calculation = calculateBatchAuctionBidFee(bidQuantity, auction.auction.core.quantityForSale, bidAmount); + const auto 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 auto auction = getAuction(auctionIndex); - const auto calculation = calculateBatchAuctionBidFee(bidQuantity, auction.auction.core.quantityForSale, bidAmount); + const auto calculation = calculateBatchAuctionBidFee(bidQuantity, bidAmount); return placeBidWithFundedReward(bidder, auctionIndex, bidQuantity, bidAmount, static_cast(calculation.requiredReward)); } @@ -367,13 +365,12 @@ class ContractTestingNOST : protected ContractTesting return input; } - NOST::CalculateBatchAuctionBidFee_output calculateBatchAuctionBidFee(uint64 bidQuantity, uint64 tokensForSale, uint64 bidAmount) const + NOST::CalculateBatchAuctionBidFee_output calculateBatchAuctionBidFee(uint64 bidQuantity, uint64 bidAmount) const { NOST::CalculateBatchAuctionBidFee_input input{}; NOST::CalculateBatchAuctionBidFee_output output{}; input.bidQuantity = bidQuantity; - input.tokensForSale = tokensForSale; input.bidAmount = bidAmount; callFunction(NOST_CONTRACT_INDEX, 20, input, output); return output; @@ -685,8 +682,7 @@ class ContractTestingNOST : protected ContractTesting 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) + uint64 minimumBidIncrement = NOST_STANDARD_MIN_BID_INCREMENT, uint64 buyNowPrice = 0) { NOST::CreateAuction_input input{}; input.metadataIpfsCid = makeMetadataCid(); @@ -1157,28 +1153,18 @@ TEST(ContractNostromoAuction, BatchBidFeeBoundariesAuction) const struct { uint64 bidQuantity; - uint64 tokensForSale; uint64 bidAmount; uint64 escrowAmount; uint64 fee; uint64 requiredReward; } cases[] = { - {1, 10, 9, 9, 101, 110}, - {1, 10, 10, 10, 100, 110}, - {1, 10, 20, 20, 99, 119}, - {1, 10, 30, 30, 98, 128}, - {10, 10, 100, 1000, 1, 1001}, - {1, 1, 101, 101, 0, 101}, - {2, 1, 101, 202, 0, 202}, - {2, 10, 19, 38, 98, 136}, - {2, 0, 100, 200, 101, 301}, - {UINT64_MAX, 1, 2, UINT64_MAX, 0, UINT64_MAX}, + {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) { - const auto output = - nostromo.calculateBatchAuctionBidFee(testCase.bidQuantity, testCase.tokensForSale, 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); @@ -1205,8 +1191,7 @@ TEST(ContractNostromoAuction, BatchAuctionCreationFeeConfigurationBoundariesAuct EXPECT_EQ(nostromo.getAuctionFees().batchAuctionCreationFee, static_cast(INT64_MAX)); auto managementInput = nostromo.makeManagementFeeInput(41); - ASSERT_EQ(nostromo.setAuctionFeesByManagement(ContractTestingNOST::managementWallet(), managementInput).errorCode, - NOST::EAuctionError::Success); + ASSERT_EQ(nostromo.setAuctionFeesByManagement(ContractTestingNOST::managementWallet(), managementInput).errorCode, NOST::EAuctionError::Success); EXPECT_EQ(nostromo.getAuctionFees().batchAuctionCreationFee, 41ULL); } @@ -1226,36 +1211,34 @@ TEST(ContractNostromoAuction, AcceptedBatchBidAccumulatesFeeAndKeepsEscrowAuctio const sint64 firstBidderBefore = getBalance(firstBidder); const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); const uint64 poolBefore = nostromo.getPendingServiceFeePool().pendingServiceFeePool; - const auto zeroNormalizedBid = nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 1, 9, 110); - EXPECT_EQ(zeroNormalizedBid.errorCode, NOST::EAuctionError::InvalidInput); - EXPECT_EQ(zeroNormalizedBid.refundedAmount, 110ULL); + 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, 10, 100); - ASSERT_EQ(calculation.escrowAmount, 200ULL); - ASSERT_EQ(calculation.fee, 81ULL); - ASSERT_EQ(calculation.requiredReward, 281ULL); - const auto underfunded = - nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 2, 100, calculation.requiredReward - 1); + 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, 280ULL); + EXPECT_EQ(underfunded.refundedAmount, 99ULL); EXPECT_EQ(getBalance(firstBidder), firstBidderBefore); - const auto accepted = - nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 2, 100, calculation.requiredReward + 49); + const auto accepted = nostromo.placeBidWithFundedReward(firstBidder, createOutput.auctionIndex, 2, 40, calculation.requiredReward + 49); ASSERT_EQ(accepted.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(accepted.escrowedAmount, 200ULL); + EXPECT_EQ(accepted.escrowedAmount, 80ULL); EXPECT_EQ(accepted.refundedAmount, 49ULL); - EXPECT_EQ(getBalance(firstBidder), firstBidderBefore - 281); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + 281); - EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, poolBefore + 81ULL); + EXPECT_EQ(getBalance(firstBidder), firstBidderBefore - 100); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + 100); + EXPECT_EQ(nostromo.getPendingServiceFeePool().pendingServiceFeePool, 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 + 200 - static_cast(poolBefore)); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + 80 - static_cast(poolBefore)); } TEST(ContractNostromoAuction, CreateStandardSingleAssetAuctionEscrowsLotAuction) @@ -1364,9 +1347,8 @@ TEST(ContractNostromoAuction, CreateStandardAuctionSupportsFourLotEntriesAuction 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}}))); + 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) { @@ -1609,8 +1591,7 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacit 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); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(allowedBidder, createOutput.auctionIndex, 1, 10).errorCode, NOST::EAuctionError::Success); } { @@ -1643,8 +1624,7 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessListsSupportMaximumViewCapacit 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); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(accessBidder, createOutput.auctionIndex, 1, 10).errorCode, NOST::EAuctionError::Success); } } @@ -1769,34 +1749,29 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) invalidStandardIncrement.minimumBidIncrement = 0; EXPECT_EQ(nostromo.createAuction(seller, 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); + auto invalidStandardLowInitial = ContractTestingNOST::makeStandardAuctionInput( + ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE - 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_BID_INCREMENT); EXPECT_EQ(nostromo.createAuction(seller, 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); + auto invalidStandardLowSale = ContractTestingNOST::makeStandardAuctionInput( + ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE - 1, NOST_STANDARD_MIN_BID_INCREMENT); EXPECT_EQ(nostromo.createAuction(seller, 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); + auto invalidStandardLowIncrement = ContractTestingNOST::makeStandardAuctionInput( + ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_BID_INCREMENT - 1); EXPECT_EQ(nostromo.createAuction(seller, 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); + auto invalidStandardPrice = ContractTestingNOST::makeStandardAuctionInput( + ContractTestingNOST::makeSingleLot(assetA, 1), NOST_STANDARD_MIN_PRICE + 1, NOST_STANDARD_MIN_PRICE, NOST_STANDARD_MIN_BID_INCREMENT); EXPECT_EQ(nostromo.createAuction(seller, invalidStandardPrice).errorCode, NOST::EAuctionError::InvalidInput); auto invalidStandardSalePrice = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); invalidStandardSalePrice.salePrice = 0; EXPECT_EQ(nostromo.createAuction(seller, 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); + 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(nostromo.createAuction(seller, invalidStandardBuyNow).errorCode, NOST::EAuctionError::InvalidInput); auto privateWithoutGate = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); @@ -2148,15 +2123,13 @@ TEST(ContractNostromoAuction, BatchMinimumPurchaseTailAndSameBidderDisplacementA 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); + 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); + 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); @@ -2177,8 +2150,7 @@ TEST(ContractNostromoAuction, BatchMinimumPurchaseTailAndSameBidderDisplacementA 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); + 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, 240ULL); @@ -2231,8 +2203,7 @@ TEST(ContractNostromoAuction, BatchAvailabilityGetterStatesAuction) 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); + 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); @@ -2282,14 +2253,12 @@ TEST(ContractNostromoAuction, PlaceStandardBidValidatesRefundsAndPauseAuction) 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); + 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); + 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); @@ -2310,23 +2279,20 @@ TEST(ContractNostromoAuction, PlaceStandardBidValidatesRefundsAndPauseAuction) 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); + 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); + 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); + 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); } @@ -2351,8 +2317,7 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) 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); + EXPECT_EQ(nostromo.placeBatchBidWithRequiredReward(allowed, createOutput.auctionIndex, 1, 12).errorCode, NOST::EAuctionError::Success); } { @@ -2446,14 +2411,11 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF 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); + 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); + 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); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidderC, createOutput.auctionIndex, 2, 20).errorCode, NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); @@ -2494,8 +2456,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF 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); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, 2, 12).errorCode, NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); @@ -2528,8 +2489,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF 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); + ASSERT_EQ(nostromo.placeBidWithFundedReward(secondBidder, createOutput.auctionIndex, 10, 15, 150).errorCode, NOST::EAuctionError::BidTooLow); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); @@ -2560,10 +2520,8 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF 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); + 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); @@ -2590,9 +2548,9 @@ TEST(ContractNostromoAuction, BatchFinalizationRefundsLosingBidsAndTieBreaksByBi const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 2, 5)); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - nostromo.seedUser(earlierBidder, 106); - nostromo.seedUser(laterBidder, 106); - nostromo.seedUser(higherBidder, 107); + 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); @@ -2600,11 +2558,9 @@ TEST(ContractNostromoAuction, BatchFinalizationRefundsLosingBidsAndTieBreaksByBi 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); + 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); + ASSERT_EQ(nostromo.placeBatchBidWithFundedRequiredReward(higherBidder, createOutput.auctionIndex, 1, 11).errorCode, NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); @@ -2623,9 +2579,9 @@ TEST(ContractNostromoAuction, BatchFinalizationRefundsLosingBidsAndTieBreaksByBi 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 - 106); - EXPECT_EQ(getBalance(laterBidder), laterBefore - 96); - EXPECT_EQ(getBalance(higherBidder), higherBefore - 107); + EXPECT_EQ(getBalance(earlierBidder), earlierBefore - 100); + EXPECT_EQ(getBalance(laterBidder), laterBefore - 90); + EXPECT_EQ(getBalance(higherBidder), higherBefore - 100); } TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) @@ -2638,7 +2594,8 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionWithoutBidAuction) 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))); + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); @@ -2661,7 +2618,8 @@ TEST(ContractNostromoAuction, WeeklyPauseShiftsActiveAuctionDeadlineAuction) 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))); + 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); @@ -2689,7 +2647,8 @@ TEST(ContractNostromoAuction, EndTickSkipsAuctionProcessingAtBootstrapTimeAuctio 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))); + 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); @@ -2716,10 +2675,9 @@ TEST(ContractNostromoAuction, PauseShiftsSellerDecisionDeadlineAuction) 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); + 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; @@ -2786,8 +2744,8 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) nostromo.calculateAuctionRevenueBreakdown(NOST_STANDARD_MIN_PRICE, expectedRevenue); const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedRevenue.shareholderDividendAmount); - const auto createOutput = nostromo.createAuction( - seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); + const auto createOutput = + nostromo.createAuction(seller, ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 1))); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); const sint64 sellerBalanceBefore = getBalance(seller); const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); @@ -2808,7 +2766,8 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) if (routeMode != 0) { EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, NOST_STANDARD_MIN_PRICE - expectedRevenue.sellerPayout); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, + NOST_STANDARD_MIN_PRICE - expectedRevenue.sellerPayout); EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); } @@ -2838,8 +2797,7 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction 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) + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 200000ULL, NOST_STANDARD_MIN_PRICE + 200000ULL) .errorCode, NOST::EAuctionError::Success); @@ -2871,8 +2829,9 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction 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) + ASSERT_EQ(nostromo + .placeBidWithFundedReward(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 200000ULL, + NOST_STANDARD_MIN_PRICE + 200000ULL) .errorCode, NOST::EAuctionError::Success); @@ -2907,8 +2866,7 @@ TEST(ContractNostromoAuction, PendingSellerDecisionAcceptRejectAndTimeoutAuction 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) + ASSERT_EQ(nostromo.placeBid(bidder, createOutput.auctionIndex, 1, NOST_STANDARD_MIN_PRICE + 200000ULL, NOST_STANDARD_MIN_PRICE + 200000ULL) .errorCode, NOST::EAuctionError::Success); @@ -3132,8 +3090,7 @@ TEST(ContractNostromoAuction, CancelAuctionRefundsBidsAndFreesParticipantSlotsAu 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); + 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); @@ -3202,8 +3159,7 @@ TEST(ContractNostromoAuction, ClosedAuctionHistoryRecordsFinalizedAndCancelledAu 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); + 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); @@ -3437,8 +3393,7 @@ TEST(ContractNostromoAuction, FeeReserveGuardTriggersEmergencyPauseOnSuddenDropA 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); + const auto blockedCreate = nostromo.createAuctionWithFundedReward(newSeller, ContractTestingNOST::makeBatchAuctionInput(blockedAsset, 2, 1), 100); EXPECT_EQ(blockedCreate.errorCode, NOST::EAuctionError::AuctionPaused); EXPECT_EQ(getBalance(newSeller), newSellerBefore); @@ -3473,12 +3428,12 @@ TEST(ContractNostromoAuction, FeeReserveGuardResamplesWindowWithoutFalseTriggerA 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% + 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% + 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); } From dde5a832c57e82743a26f445b4f07ec8c426d22f Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 9 Jul 2026 20:59:31 +0300 Subject: [PATCH 51/59] Add detailed documentation for auction-related functions; enhance code clarity --- src/contracts/Nostromo.h | 252 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index ae7eff1f5..e589ea7c8 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -1947,8 +1947,12 @@ struct NOST : public ContractBase REGISTER_USER_FUNCTION(GetFeeReserveGuardState, 22); } + /** + * @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().batchAuctionCreationFee = NOST_PUBLIC_BATCH_AUCTION_CREATION_FEE; state.mut().auctionCancellationFeeBasisPoints = NOST_DEFAULT_AUCTION_CANCELLATION_FEE_BP; @@ -1976,12 +1980,18 @@ struct NOST : public ContractBase _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() { // TODO: Change to valid epoch @@ -2014,12 +2024,14 @@ struct NOST : public ContractBase _E, _N, _Z, _O, _X, _S, _V, _O, _B, _K, _G, _Z, _C, _C, _F, _D, _B, _D, _M, _T, _M, _L, _C); } + // 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); 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) { @@ -2039,8 +2051,12 @@ struct NOST : public ContractBase } } + /** + * @brief Distributes pending service fees and performs auction storage cleanup. + */ END_EPOCH_WITH_LOCALS() { + // Service fees collected during the epoch are distributed as one batch to avoid repeated dividend dust handling. if (state.get().pendingServiceFeePool > 0) { locals.distributeAuctionServiceFeeInput.feeAmount = state.get().pendingServiceFeePool; @@ -2051,11 +2067,15 @@ struct NOST : public ContractBase state.mut().auctionList.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); @@ -2100,6 +2120,7 @@ struct NOST : public ContractBase 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) { @@ -2125,6 +2146,7 @@ struct NOST : public ContractBase } else { + // 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); @@ -2147,12 +2169,16 @@ struct NOST : public ContractBase } } + /** + * @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; @@ -2182,12 +2208,16 @@ struct NOST : public ContractBase 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) @@ -2200,6 +2230,7 @@ struct NOST : public ContractBase 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) { @@ -2211,8 +2242,12 @@ struct NOST : public ContractBase } } + /** + * @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; @@ -2228,10 +2263,14 @@ struct NOST : public ContractBase 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) @@ -2249,6 +2288,7 @@ struct NOST : public ContractBase 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) @@ -2290,6 +2330,7 @@ struct NOST : public ContractBase 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) @@ -2325,6 +2366,7 @@ struct NOST : public ContractBase 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) { @@ -2351,6 +2393,9 @@ struct NOST : public ContractBase state.mut().auctionTimerPauseEndsAt.setInvalid(); } + /** + * @brief Returns remaining launch-delay ticks after the current epoch begins. + */ PRIVATE_FUNCTION_WITH_LOCALS(GetTicksBeforeAuctionLaunchInternal) { output.ticks = 0; @@ -2365,11 +2410,15 @@ struct NOST : public ContractBase 0)); } + /** + * @brief Splits auction sale revenue between the seller and configured fee recipients. + */ 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; @@ -2379,6 +2428,7 @@ struct NOST : public ContractBase calculateAuctionRevenueBreakdown(input.grossAmount, state, locals.auctionRevenueBreakdown); output.sellerPayout = locals.auctionRevenueBreakdown.sellerPayout; + // The temporary routing switch keeps seller payout math unchanged while sending every fee to development. if (routeAllFeesToDevelopment(state)) { if (input.grossAmount > output.sellerPayout) @@ -2388,6 +2438,7 @@ struct NOST : public ContractBase } else { + // Dividend dust stays pooled until it can be distributed evenly to all computors. state.mut().auctionShareholderDividendPool = sadd(state.get().auctionShareholderDividendPool, locals.auctionRevenueBreakdown.shareholderDividendAmount); if (locals.auctionRevenueBreakdown.managementFeeAmount > 0) @@ -2414,16 +2465,21 @@ struct NOST : public ContractBase output.success = 1; } + /** + * @brief Distributes accumulated service fees to shareholders and configured recipients. + */ PRIVATE_PROCEDURE_WITH_LOCALS(DistributeAuctionServiceFee) { output.success = 0; + // Creation and cancellation paths may call this with zero after fee configuration changes. if (input.feeAmount == 0) { output.success = 1; return; } + // Service fees use fixed recipients unless the runtime override sends all fees to development. if (routeAllFeesToDevelopment(state)) { qpi.transfer(state.get().development, input.feeAmount); @@ -2457,6 +2513,9 @@ struct NOST : public ContractBase output.success = 1; } + /** + * @brief Counts non-empty wallet entries allowed to bid in a private auction. + */ PRIVATE_FUNCTION_WITH_LOCALS(CountAllowedBidderWallets) { output.allowedWalletCount = 0; @@ -2469,10 +2528,14 @@ struct NOST : public ContractBase } } + /** + * @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) { @@ -2497,6 +2560,9 @@ struct NOST : public ContractBase } } + /** + * @brief Checks whether the invocator owns at least one configured access asset. + */ PRIVATE_FUNCTION_WITH_LOCALS(HasRequiredAccessAsset) { output.hasRequiredAccessAsset = 0; @@ -2505,6 +2571,7 @@ struct NOST : public ContractBase 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)) @@ -2521,6 +2588,9 @@ struct NOST : public ContractBase } } + /** + * @brief Recomputes the displayed highest active Batch Auction bid. + */ PRIVATE_PROCEDURE_WITH_LOCALS(RecomputeBatchHighestBid) { locals.bestParticipantFound = 0; @@ -2535,6 +2605,7 @@ struct NOST : public ContractBase 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); @@ -2580,6 +2651,9 @@ struct NOST : public ContractBase 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; @@ -2603,6 +2677,7 @@ struct NOST : public ContractBase 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); @@ -2637,6 +2712,7 @@ struct NOST : public ContractBase 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; @@ -2665,6 +2741,7 @@ struct NOST : public ContractBase 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) { @@ -2694,6 +2771,9 @@ struct NOST : public ContractBase 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; @@ -2752,6 +2832,7 @@ struct NOST : public ContractBase return; } + // Batch bids always consume a new participant slot; historical slots remain readable after settlement. locals.freeParticipantSlotFound = 0; for (locals.participantIndex = 0; locals.participantIndex < state.get().participants.capacity(); ++locals.participantIndex) { @@ -2783,6 +2864,7 @@ struct NOST : public ContractBase 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) { @@ -2793,6 +2875,7 @@ struct NOST : public ContractBase 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) { @@ -2870,6 +2953,7 @@ struct NOST : public ContractBase 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) { state.mut().pendingServiceFeePool = sadd(state.get().pendingServiceFeePool, locals.bidFeeCalculation.fee); @@ -2886,6 +2970,9 @@ struct NOST : public ContractBase output.success = 1; } + /** + * @brief Validates and records a Standard Auction bid, refunding replaced escrow. + */ PRIVATE_PROCEDURE_WITH_LOCALS(ProcessStandardBid) { output.escrowedAmount = 0; @@ -2930,6 +3017,7 @@ struct NOST : public ContractBase 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); @@ -2977,6 +3065,7 @@ struct NOST : public ContractBase 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()) { @@ -3015,6 +3104,7 @@ struct NOST : public ContractBase 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) { qpi.transfer(qpi.invocator(), locals.previousEscrow); @@ -3029,6 +3119,7 @@ struct NOST : public ContractBase 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; @@ -3037,17 +3128,22 @@ struct NOST : public ContractBase } } + /** + * @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); @@ -3079,9 +3175,13 @@ struct NOST : public ContractBase 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); @@ -3100,8 +3200,12 @@ struct NOST : public ContractBase } } + /** + * @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); @@ -3114,6 +3218,9 @@ struct NOST : public ContractBase } } + /** + * @brief Settles a Batch Auction by allocating winning quantities and closing the auction. + */ PRIVATE_PROCEDURE_WITH_LOCALS(FinalizeBatchAuction) { output.success = 0; @@ -3261,6 +3368,9 @@ struct NOST : public ContractBase 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; @@ -3285,6 +3395,7 @@ struct NOST : public ContractBase 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; @@ -3308,12 +3419,14 @@ struct NOST : public ContractBase } 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) @@ -3329,6 +3442,9 @@ struct NOST : public ContractBase 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; @@ -3353,6 +3469,7 @@ struct NOST : public ContractBase 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) { qpi.transfer(locals.highestBidderData.participant, locals.highestBidderData.escrowedAmount); @@ -3364,6 +3481,7 @@ struct NOST : public ContractBase 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); @@ -3381,9 +3499,13 @@ struct NOST : public ContractBase 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); @@ -3427,6 +3549,7 @@ struct NOST : public ContractBase { output.errorCode = EAuctionError::InvalidInput; + // Any rejection before escrow succeeds refunds the full invocation reward. CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); if (locals.isAuctionInteractionPausedOutput.isPaused) { @@ -3523,6 +3646,7 @@ struct NOST : public ContractBase 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)) @@ -3572,6 +3696,7 @@ struct NOST : public ContractBase return; } + // Private auctions must choose exactly one access gate: wallet list or asset ownership. locals.countAllowedBidderWalletsInput.allowedBidderWallets = input.allowedBidderWallets; CALL(CountAllowedBidderWallets, locals.countAllowedBidderWalletsInput, locals.countAllowedBidderWalletsOutput); locals.countRequiredAccessAssetsInput.requiredAccessAssets = input.requiredAccessAssets; @@ -3622,6 +3747,7 @@ struct NOST : public ContractBase return; } + // From this point onward, asset escrow may need explicit rollback on storage failure. locals.escrowAuctionLotAssetsInput.auctionLotItems = input.auctionLotItems; CALL(EscrowAuctionLotAssets, locals.escrowAuctionLotAssetsInput, locals.escrowAuctionLotAssetsOutput); if (!locals.escrowAuctionLotAssetsOutput.success) @@ -3649,6 +3775,7 @@ struct NOST : public ContractBase 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) { @@ -3673,6 +3800,7 @@ struct NOST : public ContractBase 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; @@ -3689,6 +3817,7 @@ struct NOST : public ContractBase return; } + // Creation fees are held until END_EPOCH; overpayment is returned immediately. if (locals.requiredFee > 0) { state.mut().pendingServiceFeePool = sadd(state.get().pendingServiceFeePool, static_cast(locals.requiredFee)); @@ -3721,6 +3850,7 @@ struct NOST : public ContractBase { output.errorCode = EAuctionError::InvalidInput; + // Common auction gates run before type-specific bid processing; failed gates refund the reward. CALL(IsAuctionInteractionPaused, locals.isAuctionInteractionPausedInput, locals.isAuctionInteractionPausedOutput); if (locals.isAuctionInteractionPausedOutput.isPaused) { @@ -3784,6 +3914,7 @@ struct NOST : public ContractBase return; } + // Private access accepts either the configured asset gate or the configured wallet gate. if (locals.auction.core.visibility == EAuctionVisibility::Private) { if (locals.auction.requiredAccessAssets.population() > 0) @@ -3811,6 +3942,7 @@ struct NOST : public ContractBase } } + // Type-specific processors own escrow/refund details once common validation succeeds. switch (locals.auction.core.type) { case EAuctionType::Batch: @@ -3885,6 +4017,7 @@ struct NOST : public ContractBase output.cancellationFee = 0; output.errorCode = EAuctionError::InvalidInput; + // Cancellation is blocked during emergency pause but does not use the scheduled auction timer pause. if (state.get().isEmergencyPaused) { if (qpi.invocationReward() > 0) @@ -3937,6 +4070,7 @@ struct NOST : public ContractBase return; } + // The fee base represents the full reserve value of the lot being withdrawn. locals.cancellationBaseAmount = locals.auction.core.salePrice; if (locals.auction.core.type == EAuctionType::Batch) { @@ -3958,6 +4092,7 @@ struct NOST : public ContractBase return; } + // Clear any participant escrow defensively before returning the seller's lot. locals.participantIndex = 0; while (locals.participantIndex < state.get().participants.capacity()) { @@ -3980,6 +4115,7 @@ struct NOST : public ContractBase 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; @@ -3992,6 +4128,7 @@ struct NOST : public ContractBase state.mut().auctionList.replace(input.auctionIndex, locals.auction); addClosedAuctionToHistory(state, locals.auction.core.auctionIndex); + // Cancellation fees are distributed immediately because cancellation is already a settlement action. locals.distributeAuctionServiceFeeInput.feeAmount = output.cancellationFee; CALL(DistributeAuctionServiceFee, locals.distributeAuctionServiceFeeInput, locals.distributeAuctionServiceFeeOutput); @@ -4014,6 +4151,7 @@ struct NOST : public ContractBase 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()); @@ -4065,6 +4203,7 @@ struct NOST : public ContractBase 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) { @@ -4079,6 +4218,7 @@ struct NOST : public ContractBase return; } + // Accepting finalizes the sale; rejecting refunds the bidder and returns the lot to the seller. if (input.acceptSale) { locals.finalizeStandardAuctionInput.auctionIndex = input.auctionIndex; @@ -4106,6 +4246,7 @@ struct NOST : public ContractBase 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()); @@ -4119,6 +4260,7 @@ struct NOST : public ContractBase return; } + // Validate all fee tiers together so no gross-proceeds tier can exceed 100 percent. if (!isValidAuctionFeeConfiguration(input.privateAuctionFee, input.batchAuctionCreationFee, input.auctionCancellationFeeBasisPoints, input.managementFeeBasisPoints, input.developmentFeeBasisPoints, input.takeoverCoordinatorFeeBasisPoints, input.shareholderDividendBasisPoints, input.shareholderFeeBasisPointsTier1, @@ -4155,6 +4297,7 @@ struct NOST : public ContractBase { 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()); @@ -4236,6 +4379,7 @@ struct NOST : public ContractBase 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()); @@ -4273,6 +4417,7 @@ struct NOST : public ContractBase 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()); @@ -4318,6 +4463,7 @@ struct NOST : public ContractBase 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; @@ -4348,6 +4494,7 @@ struct NOST : public ContractBase { 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); @@ -4461,6 +4608,9 @@ struct NOST : public ContractBase output.isEmergencyPaused = state.get().isEmergencyPaused; } + /** + * @brief Returns aggregate auction, participant, fee, and pause counters. + */ PUBLIC_FUNCTION_WITH_LOCALS(GetContractStats) { output.stats.totalAuctionsCreated = state.get().totalAuctionsCreated; @@ -4473,6 +4623,7 @@ struct NOST : public ContractBase output.stats.isPostBeginEpochPauseArmed = state.get().isPostBeginEpochPauseArmed; output.stats.isEmergencyPaused = state.get().isEmergencyPaused; + // Stats scan fixed storage because participant slots and auction records are not separately indexed by status. for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participants.capacity(); ++locals.participantSlotIndex) { locals.participantData = state.get().participants.get(locals.participantSlotIndex); @@ -4501,11 +4652,15 @@ struct NOST : public ContractBase } } + /** + * @brief Returns a page of auction summaries ordered by creation index. + */ PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionSummaries) { output.totalCount = state.get().totalAuctionsCreated; output.returnedCount = 0; locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + // This getter paginates by creation index, so skipped missing records still count in `totalCount`. for (locals.auctionIndex = input.offset; locals.auctionIndex < state.get().totalAuctionsCreated && output.returnedCount < locals.boundedLimit; ++locals.auctionIndex) { @@ -4519,11 +4674,15 @@ struct NOST : public ContractBase } } + /** + * @brief Returns a page of active or pending-seller-decision auction indices. + */ PUBLIC_FUNCTION_WITH_LOCALS(GetActiveAuctionIndices) { output.totalCount = 0; output.returnedCount = 0; locals.boundedLimit = min(input.limit, NOST_AUCTION_GETTER_PAGE_SIZE); + // Filtered getters count matches before pagination so callers can request the next page. for (locals.auctionIndex = 0; locals.auctionIndex < state.get().totalAuctionsCreated; ++locals.auctionIndex) { if (!state.get().auctionList.get(locals.auctionIndex, locals.auction)) @@ -4543,6 +4702,9 @@ struct NOST : public ContractBase } } + /** + * @brief Returns a page of auction summaries created by a seller. + */ PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionsBySeller) { output.totalCount = 0; @@ -4564,10 +4726,14 @@ struct NOST : public ContractBase } } + /** + * @brief Looks up the first auction matching a metadata CID. + */ PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionByMetadataCid) { output.found = 0; output.auctionIndex = 0; + // Metadata lookup returns the first matching auction in creation order. for (locals.auctionIndex = 0; locals.auctionIndex < state.get().totalAuctionsCreated; ++locals.auctionIndex) { if (!state.get().auctionList.get(locals.auctionIndex, locals.auction)) @@ -4593,10 +4759,14 @@ struct NOST : public ContractBase } } + /** + * @brief Returns auction summaries for a batch of requested indices. + */ PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionSummariesByIndexBatch) { 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) { locals.auctionIndex = input.auctionIndices.get(locals.requestedIndex); @@ -4610,11 +4780,15 @@ struct NOST : public ContractBase } } + /** + * @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) { locals.participantData = state.get().participants.get(locals.participantSlotIndex); @@ -4632,11 +4806,15 @@ struct NOST : public ContractBase } } + /** + * @brief Returns a page of historical auction participations for one wallet. + */ PUBLIC_FUNCTION_WITH_LOCALS(GetUserParticipations) { 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) { locals.participantData = state.get().participants.get(locals.participantSlotIndex); @@ -4654,12 +4832,18 @@ struct NOST : public ContractBase } } + /** + * @brief Returns the most recently created auction index when one exists. + */ PUBLIC_FUNCTION(GetLatestAuctionIndex) { output.found = state.get().totalAuctionsCreated > 0; output.auctionIndex = output.found ? state.get().totalAuctionsCreated - 1 : 0; } + /** + * @brief Counts auctions created by a seller. + */ PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionCountBySeller) { output.count = 0; @@ -4672,6 +4856,9 @@ struct NOST : public ContractBase } } + /** + * @brief Returns immutable creation-time fields for an auction. + */ PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionAtCreationSnapshot) { output.found = 0; @@ -4718,6 +4905,7 @@ struct NOST : public ContractBase 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) @@ -4731,6 +4919,7 @@ struct NOST : public ContractBase return; } + // `releaseShares` consumes only the destination transfer fee; any unused reward is refunded below. if (input.numberOfShares > 0 && qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) >= input.numberOfShares) { @@ -4760,6 +4949,9 @@ struct NOST : public ContractBase } protected: + /** + * @brief Emits a procedure log as success or error based on its error code. + */ static void logProcedureResult(const NostromoProcedureLog& log) { if (log.errorCode == static_cast(EAuctionError::Success)) @@ -4772,6 +4964,9 @@ struct NOST : public ContractBase } } + /** + * @brief Fills the common procedure log payload. + */ static void setProcedureLogInput(NostromoProcedureLog& log, const id& actor, EProcedureId procedure, EAuctionError errorCode, uint64 auctionIndex, sint64 amount) { @@ -4784,6 +4979,9 @@ struct NOST : public ContractBase log._terminator = 0; } + /** + * @brief Copies persisted auction data into a compact summary. + */ static void fillAuctionSummary(const AuctionData& auction, AuctionSummary& summary) { summary.metadataIpfsCid = auction.core.metadataIpfsCid; @@ -4805,6 +5003,9 @@ struct NOST : public ContractBase summary.status = static_cast(auction.core.status); } + /** + * @brief Copies participant storage data into an auction participant summary. + */ static void fillParticipantSummary(const AuctionParticipantData& participantData, ParticipantSummary& summary) { summary.participant = participantData.participant; @@ -4816,6 +5017,9 @@ struct NOST : public ContractBase summary.isWinningBid = participantData.isWinningBid; } + /** + * @brief Copies participant storage data into a user participation summary. + */ static void fillUserParticipationSummary(uint64 auctionIndex, const AuctionParticipantData& participantData, UserParticipationSummary& summary) { summary.participant = participantData.participant; @@ -4828,17 +5032,26 @@ struct NOST : public ContractBase summary.isWinningBid = participantData.isWinningBid; } + /** + * @brief Returns the smaller of two values. + */ template static constexpr T min(const T& a, const T& b) { 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; } + /** + * @brief Resolves Batch Auction quantity invariants from creation input. + */ static bool resolveBatchAuctionCreateParams(uint64 lotItemCount, uint64 totalEscrowQuantity, uint64 minimumPurchaseQuantity, uint64& quantityForSale, uint64& resolvedMinimumPurchaseQuantity, uint64 buyNowPrice) { @@ -4854,6 +5067,9 @@ struct NOST : public ContractBase return true; } + /** + * @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) { @@ -4880,11 +5096,17 @@ struct NOST : public ContractBase return true; } + /** + * @brief Validates that private auctions use exactly one supported access mode. + */ constexpr static bool validatePrivateAuctionAccess(EAuctionVisibility visibility, uint64 requiredAccessAssetCount, uint64 allowedWalletCount) { return visibility != EAuctionVisibility::Private || ((requiredAccessAssetCount > 0) != (allowedWalletCount > 0)); } + /** + * @brief Validates governance fee percentages and fixed service fees. + */ constexpr static bool isValidAuctionFeeConfiguration(sint64 privateAuctionFee, uint64 batchAuctionCreationFee, uint64 auctionCancellationFeeBasisPoints, uint64 managementFeeBasisPoints, uint64 developmentFeeBasisPoints, uint64 takeoverCoordinatorFeeBasisPoints, @@ -4907,6 +5129,9 @@ struct NOST : public ContractBase NOST_BASIS_POINTS_SCALE; } + /** + * @brief Selects the shareholder fee tier for a gross auction amount. + */ static uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount, const StateData& state) { if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_1) @@ -4924,6 +5149,9 @@ struct NOST : public ContractBase return state.shareholderFeeBasisPointsTier4; } + /** + * @brief Selects the shareholder fee tier from contract state. + */ static uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount, const ContractState& state) { return getAuctionShareholderFeeBasisPoints(grossAmount, state.get()); @@ -4965,6 +5193,9 @@ struct NOST : public ContractBase output.developmentFeeAmount - output.takeoverCoordinatorFeeAmount); } + /** + * @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); @@ -4979,6 +5210,9 @@ struct NOST : public ContractBase output.requiredReward = sadd(output.escrowAmount, output.fee); } + /** + * @brief Returns the service fee required to create an auction. + */ static sint64 getCreateAuctionFee(EAuctionType auctionType, EAuctionVisibility visibility, const ContractState& state) { if (visibility == EAuctionVisibility::Private) @@ -4990,16 +5224,25 @@ struct NOST : public ContractBase : 0; } + /** + * @brief Returns whether an auction type is accepted by the contract. + */ static bool isSupportedAuctionType(EAuctionType auctionType) { return auctionType == EAuctionType::Batch || auctionType == EAuctionType::Standard; } + /** + * @brief Returns whether an auction visibility is accepted by the contract. + */ static bool isSupportedAuctionVisibility(EAuctionVisibility visibility) { return visibility == EAuctionVisibility::Public || visibility == EAuctionVisibility::Private; } + /** + * @brief Returns whether an asset entry is empty. + */ static bool isZeroAsset(const Asset& asset) { return asset.assetName == 0 && isZero(asset.issuer); } /** @brief Returns whether the runtime fee override routes every auction fee to the development wallet. */ @@ -5008,17 +5251,26 @@ struct NOST : public ContractBase return state.get().routeAllFeesToDevelopment; } + /** + * @brief Appends a closed auction index to the ring-buffer history. + */ static void addClosedAuctionToHistory(QPI::ContractState& state, uint64 auctionIndex) { state.mut().closedAuctionHistory.set(mod(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()), auctionIndex); state.mut().closedAuctionHistoryCounter = sadd(state.get().closedAuctionHistoryCounter, 1ULL); } + /** + * @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); } + /** + * @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) { From 416422c0414696f36548b6972e192caf5ebb3041 Mon Sep 17 00:00:00 2001 From: N-010 Date: Sat, 18 Jul 2026 19:38:23 +0300 Subject: [PATCH 52/59] Refactor auction creation fee handling; unify public auction fee constants and update related logic --- src/contracts/Nostromo.h | 121 +++++++++++++++++++----------------- test/contract_nostromo.cpp | 124 ++++++++++++++++++++++++++----------- 2 files changed, 154 insertions(+), 91 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index e589ea7c8..0bb6879e0 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -32,8 +32,8 @@ constexpr uint64 NOST_AUCTION_REQUIRED_ACCESS_ASSET_NUM = 4; 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 Batch Auction and distributed at END_EPOCH, in qu. -constexpr uint64 NOST_PUBLIC_BATCH_AUCTION_CREATION_FEE = 100LL; +// 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. @@ -359,8 +359,8 @@ struct NOST : public ContractBase /** @brief Configured fee charged when creating a private auction. */ sint64 privateAuctionFee; - /** @brief Configured fee accumulated when creating a public Batch Auction and distributed at `END_EPOCH`. */ - uint64 batchAuctionCreationFee; + /** @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; @@ -576,8 +576,8 @@ struct NOST : public ContractBase /** @brief Fee charged when a private auction is created. */ sint64 privateAuctionFee; - /** @brief Fee accumulated when a public Batch Auction is created and distributed at `END_EPOCH`; must not exceed `INT64_MAX`. */ - uint64 batchAuctionCreationFee; + /** @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; @@ -619,8 +619,8 @@ struct NOST : public ContractBase /** @brief Fee charged when a private auction is created. */ sint64 privateAuctionFee; - /** @brief Fee accumulated when a public Batch Auction is created and distributed at `END_EPOCH`; must not exceed `INT64_MAX`. */ - uint64 batchAuctionCreationFee; + /** @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; @@ -783,8 +783,8 @@ struct NOST : public ContractBase /** @brief Fee charged when a private auction is created. */ sint64 privateAuctionFee; - /** @brief Fee accumulated when a public Batch Auction is created and distributed at `END_EPOCH`. */ - uint64 batchAuctionCreationFee; + /** @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; @@ -1954,7 +1954,7 @@ struct NOST : public ContractBase { // Install the default governance, fee, pause, and guard configuration into the zeroed contract state. state.mut().privateAuctionFee = NOST_DEFAULT_PRIVATE_AUCTION_FEE; - state.mut().batchAuctionCreationFee = NOST_PUBLIC_BATCH_AUCTION_CREATION_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; @@ -1999,7 +1999,7 @@ struct NOST : public ContractBase { // Initialize state.mut().privateAuctionFee = NOST_DEFAULT_PRIVATE_AUCTION_FEE; - state.mut().batchAuctionCreationFee = NOST_PUBLIC_BATCH_AUCTION_CREATION_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; @@ -2125,44 +2125,54 @@ struct NOST : public ContractBase while (locals.auctionIndex != NULL_INDEX) { locals.auction = state.get().auctionList.value(locals.auctionIndex); - if (locals.auction.core.status == EAuctionStatus::Active) + switch (locals.auction.core.status) { - diffDateInSecond(locals.auction.core.createdAt, locals.currentDate, locals.elapsedSeconds); - if (locals.elapsedSeconds >= locals.auction.core.auctionDurationSeconds) - { + case EAuctionStatus::Active: + diffDateInSecond(locals.auction.core.createdAt, locals.currentDate, locals.elapsedSeconds); + 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: + 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 + { + // 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::Batch: - locals.finalizeBatchAuctionInput.auctionIndex = locals.auction.core.auctionIndex; - locals.finalizeBatchAuctionInput.currentDate = locals.currentDate; - CALL(FinalizeBatchAuction, locals.finalizeBatchAuctionInput, locals.finalizeBatchAuctionOutput); - break; case EAuctionType::Standard: - if (locals.auction.core.highestBidAmount == 0 || locals.auction.core.highestBidPrice >= locals.auction.core.salePrice) + 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); } - else - { - // 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; - }; - } - } - else if (locals.auction.core.status == EAuctionStatus::PendingSellerDecision && - 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; } locals.auctionIndex = state.get().auctionList.nextElementIndex(locals.auctionIndex); @@ -3540,8 +3550,8 @@ struct NOST : public ContractBase * @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 Auction accumulates the configured creation fee, distributed at `END_EPOCH`. Public Standard Auctions remain - * free, and failed creation refunds the full reward. + * @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 exactly one access mode. */ @@ -3717,8 +3727,7 @@ struct NOST : public ContractBase return; } - locals.requiredFee = - getCreateAuctionFee(static_cast(input.auctionType), static_cast(input.auctionVisibility), state); + locals.requiredFee = getCreateAuctionFee(static_cast(input.auctionVisibility), state); if (qpi.invocationReward() < locals.requiredFee) { if (qpi.invocationReward() > 0) @@ -4261,7 +4270,7 @@ struct NOST : public ContractBase } // Validate all fee tiers together so no gross-proceeds tier can exceed 100 percent. - if (!isValidAuctionFeeConfiguration(input.privateAuctionFee, input.batchAuctionCreationFee, input.auctionCancellationFeeBasisPoints, + if (!isValidAuctionFeeConfiguration(input.privateAuctionFee, input.publicAuctionCreationFee, input.auctionCancellationFeeBasisPoints, input.managementFeeBasisPoints, input.developmentFeeBasisPoints, input.takeoverCoordinatorFeeBasisPoints, input.shareholderDividendBasisPoints, input.shareholderFeeBasisPointsTier1, input.shareholderFeeBasisPointsTier2, input.shareholderFeeBasisPointsTier3, @@ -4274,7 +4283,7 @@ struct NOST : public ContractBase } state.mut().privateAuctionFee = input.privateAuctionFee; - state.mut().batchAuctionCreationFee = input.batchAuctionCreationFee; + state.mut().publicAuctionCreationFee = input.publicAuctionCreationFee; state.mut().auctionCancellationFeeBasisPoints = input.auctionCancellationFeeBasisPoints; state.mut().managementFeeBasisPoints = input.managementFeeBasisPoints; state.mut().developmentFeeBasisPoints = input.developmentFeeBasisPoints; @@ -4311,7 +4320,7 @@ struct NOST : public ContractBase return; } - if (!isValidAuctionFeeConfiguration(input.privateAuctionFee, input.batchAuctionCreationFee, input.auctionCancellationFeeBasisPoints, + if (!isValidAuctionFeeConfiguration(input.privateAuctionFee, input.publicAuctionCreationFee, input.auctionCancellationFeeBasisPoints, input.managementFeeBasisPoints, input.developmentFeeBasisPoints, state.get().takeoverCoordinatorFeeBasisPoints, state.get().shareholderDividendBasisPoints, input.shareholderFeeBasisPointsTier1, input.shareholderFeeBasisPointsTier2, @@ -4324,7 +4333,7 @@ struct NOST : public ContractBase } state.mut().privateAuctionFee = input.privateAuctionFee; - state.mut().batchAuctionCreationFee = input.batchAuctionCreationFee; + state.mut().publicAuctionCreationFee = input.publicAuctionCreationFee; state.mut().auctionCancellationFeeBasisPoints = input.auctionCancellationFeeBasisPoints; state.mut().managementFeeBasisPoints = input.managementFeeBasisPoints; state.mut().developmentFeeBasisPoints = input.developmentFeeBasisPoints; @@ -4550,7 +4559,7 @@ struct NOST : public ContractBase output.shareholderFeeBasisPointsTier2 = state.get().shareholderFeeBasisPointsTier2; output.shareholderFeeBasisPointsTier3 = state.get().shareholderFeeBasisPointsTier3; output.shareholderFeeBasisPointsTier4 = state.get().shareholderFeeBasisPointsTier4; - output.batchAuctionCreationFee = state.get().batchAuctionCreationFee; + output.publicAuctionCreationFee = state.get().publicAuctionCreationFee; } /** @@ -5107,14 +5116,14 @@ struct NOST : public ContractBase /** * @brief Validates governance fee percentages and fixed service fees. */ - constexpr static bool isValidAuctionFeeConfiguration(sint64 privateAuctionFee, uint64 batchAuctionCreationFee, + 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) { - return privateAuctionFee >= 0 && batchAuctionCreationFee <= UINT64_MAX && auctionCancellationFeeBasisPoints <= NOST_BASIS_POINTS_SCALE && + 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 && @@ -5213,15 +5222,15 @@ struct NOST : public ContractBase /** * @brief Returns the service fee required to create an auction. */ - static sint64 getCreateAuctionFee(EAuctionType auctionType, EAuctionVisibility visibility, const ContractState& state) + static sint64 getCreateAuctionFee(EAuctionVisibility visibility, const ContractState& state) { - if (visibility == EAuctionVisibility::Private) + switch (visibility) { - return state.get().privateAuctionFee; + case EAuctionVisibility::Public: return state.get().publicAuctionCreationFee; break; + case EAuctionVisibility::Private: return state.get().privateAuctionFee; break; + default: break; } - return auctionType == EAuctionType::Batch && visibility == EAuctionVisibility::Public - ? static_cast(state.get().batchAuctionCreationFee) - : 0; + return 0; } /** diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index ee3e77c0b..825a5becd 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -141,7 +141,7 @@ class ContractTestingNOST : protected ContractTesting } NOST::CreateAuction_output createAuction(const id& seller, const NOST::CreateAuction_input& input, - sint64 reward = NOST_PUBLIC_BATCH_AUCTION_CREATION_FEE) + sint64 reward = NOST_PUBLIC_AUCTION_CREATION_FEE) { if (reward > 0) { @@ -331,12 +331,12 @@ class ContractTestingNOST : protected ContractTesting return output; } - NOST::SetAuctionFees_input makeCoordinatorFeeInput(uint64 batchAuctionCreationFee) const + NOST::SetAuctionFees_input makeCoordinatorFeeInput(sint64 publicAuctionCreationFee) const { const auto fees = getAuctionFees(); NOST::SetAuctionFees_input input{}; input.privateAuctionFee = fees.privateAuctionFee; - input.batchAuctionCreationFee = batchAuctionCreationFee; + input.publicAuctionCreationFee = publicAuctionCreationFee; input.auctionCancellationFeeBasisPoints = fees.auctionCancellationFeeBasisPoints; input.managementFeeBasisPoints = fees.managementFeeBasisPoints; input.developmentFeeBasisPoints = fees.developmentFeeBasisPoints; @@ -349,12 +349,12 @@ class ContractTestingNOST : protected ContractTesting return input; } - NOST::SetAuctionFeesByManagement_input makeManagementFeeInput(uint64 batchAuctionCreationFee) const + NOST::SetAuctionFeesByManagement_input makeManagementFeeInput(sint64 publicAuctionCreationFee) const { const auto fees = getAuctionFees(); NOST::SetAuctionFeesByManagement_input input{}; input.privateAuctionFee = fees.privateAuctionFee; - input.batchAuctionCreationFee = batchAuctionCreationFee; + input.publicAuctionCreationFee = publicAuctionCreationFee; input.auctionCancellationFeeBasisPoints = fees.auctionCancellationFeeBasisPoints; input.managementFeeBasisPoints = fees.managementFeeBasisPoints; input.developmentFeeBasisPoints = fees.developmentFeeBasisPoints; @@ -780,7 +780,7 @@ TEST(ContractNostromoAuction, InitialStateAndGettersAuction) const auto fees = nostromo.getAuctionFees(); EXPECT_EQ(fees.privateAuctionFee, NOST_DEFAULT_PRIVATE_AUCTION_FEE); - EXPECT_EQ(fees.batchAuctionCreationFee, static_cast(NOST_PUBLIC_BATCH_AUCTION_CREATION_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); @@ -1068,15 +1068,15 @@ TEST(ContractNostromoAuction, CreateBatchPublicAuctionEscrowsLotAuction) EXPECT_EQ(nostromo.sharesManagedBy(asset, NOST_CONTRACT_ID, NOST_CONTRACT_INDEX), 9); } -TEST(ContractNostromoAuction, PublicBatchCreationAccumulatesConfiguredFeeAndRefundsExcessAuction) +TEST(ContractNostromoAuction, PublicAuctionCreationAccumulatesConfiguredFeeAndRefundsExcessAuction) { ContractTestingNOST nostromo; const id seller(901, 902, 903, 904); const Asset asset{seller, assetNameFromString("BCRFEE")}; - constexpr uint64 configuredFee = 73; + 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().batchAuctionCreationFee, configuredFee); + ASSERT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, configuredFee); EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 3), 3); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); @@ -1094,37 +1094,60 @@ TEST(ContractNostromoAuction, PublicBatchCreationAccumulatesConfiguredFeeAndRefu const auto exact = nostromo.createAuctionWithFundedReward(seller, input, configuredFee); ASSERT_EQ(exact.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(getBalance(seller), sellerBefore - static_cast(configuredFee)); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + static_cast(configuredFee)); - expectedPool += configuredFee; + 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 - static_cast(2 * configuredFee)); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + static_cast(2 * configuredFee)); - expectedPool += configuredFee; + 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 uint64 managementConfiguredFee = 29; + 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().batchAuctionCreationFee, managementConfiguredFee); + 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 - static_cast(2 * configuredFee + managementConfiguredFee)); - EXPECT_EQ(getBalance(NOST_CONTRACT_ID), contractBefore + static_cast(2 * configuredFee + managementConfiguredFee)); - expectedPool += managementConfiguredFee; + 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, 1), 1); - ASSERT_EQ(nostromo.transferShareManagementRightsToNostromo(standardSeller, standardAsset, 1), 1); + 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)); - EXPECT_EQ(nostromo.createAuctionWithFundedReward(standardSeller, standardInput, 0).errorCode, NOST::EAuctionError::Success); + 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); @@ -1143,6 +1166,22 @@ TEST(ContractNostromoAuction, PublicBatchCreationAccumulatesConfiguredFeeAndRefu 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); } @@ -1171,14 +1210,14 @@ TEST(ContractNostromoAuction, BatchBidFeeBoundariesAuction) } } -TEST(ContractNostromoAuction, BatchAuctionCreationFeeConfigurationBoundariesAuction) +TEST(ContractNostromoAuction, PublicAuctionCreationFeeConfigurationBoundariesAuction) { ContractTestingNOST nostromo; - EXPECT_EQ(nostromo.getAuctionFees().batchAuctionCreationFee, static_cast(NOST_PUBLIC_BATCH_AUCTION_CREATION_FEE)); + 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().batchAuctionCreationFee, 0ULL); + 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); @@ -1186,13 +1225,22 @@ TEST(ContractNostromoAuction, BatchAuctionCreationFeeConfigurationBoundariesAuct EXPECT_EQ(nostromo.createAuctionWithFundedReward(zeroFeeSeller, ContractTestingNOST::makeBatchAuctionInput(zeroFeeAsset, 1, 1), 0).errorCode, NOST::EAuctionError::Success); - coordinatorInput.batchAuctionCreationFee = static_cast(INT64_MAX); + coordinatorInput.publicAuctionCreationFee = INT64_MAX; ASSERT_EQ(nostromo.setAuctionFees(ContractTestingNOST::takeoverCoordinatorWallet(), coordinatorInput).errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(nostromo.getAuctionFees().batchAuctionCreationFee, static_cast(INT64_MAX)); + 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().batchAuctionCreationFee, 41ULL); + EXPECT_EQ(nostromo.getAuctionFees().publicAuctionCreationFee, 41LL); } TEST(ContractNostromoAuction, AcceptedBatchBidAccumulatesFeeAndKeepsEscrowAuction) @@ -1254,10 +1302,16 @@ TEST(ContractNostromoAuction, CreateStandardSingleAssetAuctionEscrowsLotAuction) auto input = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(asset, 5)); input.minimumPurchaseQuantity = UINT64_MAX; + nostromo.seedUser(seller, 1000); const sint64 sellerBalanceBefore = getBalance(seller); - const auto output = nostromo.createAuctionWithFundedReward(seller, input, 0); + 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); + 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); @@ -3207,7 +3261,7 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) NOST::SetAuctionFees_input coordinatorInput{}; coordinatorInput.privateAuctionFee = 60000000; - coordinatorInput.batchAuctionCreationFee = 123; + coordinatorInput.publicAuctionCreationFee = 123; coordinatorInput.auctionCancellationFeeBasisPoints = 900; coordinatorInput.managementFeeBasisPoints = 60; coordinatorInput.developmentFeeBasisPoints = 70; @@ -3237,7 +3291,7 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) EXPECT_EQ(fees.takeoverCoordinatorFeeBasisPoints, 80ULL); EXPECT_EQ(fees.shareholderDividendBasisPoints, 8500ULL); EXPECT_EQ(fees.shareholderFeeBasisPointsTier1, 400ULL); - EXPECT_EQ(fees.batchAuctionCreationFee, 123ULL); + EXPECT_EQ(fees.publicAuctionCreationFee, 123LL); const auto setManagementForbidden = nostromo.setManagement(outsider, newManagement); EXPECT_EQ(setManagementForbidden.errorCode, NOST::EAuctionError::Forbidden); @@ -3251,7 +3305,7 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) NOST::SetAuctionFeesByManagement_input managementInput{}; managementInput.privateAuctionFee = 70000000; - managementInput.batchAuctionCreationFee = 456; + managementInput.publicAuctionCreationFee = 456; managementInput.auctionCancellationFeeBasisPoints = 800; managementInput.managementFeeBasisPoints = 90; managementInput.developmentFeeBasisPoints = 110; @@ -3274,7 +3328,7 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) fees = nostromo.getAuctionFees(); EXPECT_EQ(fees.privateAuctionFee, 70000000); - EXPECT_EQ(fees.batchAuctionCreationFee, 456ULL); + EXPECT_EQ(fees.publicAuctionCreationFee, 456LL); EXPECT_EQ(fees.auctionCancellationFeeBasisPoints, 800ULL); EXPECT_EQ(fees.managementFeeBasisPoints, 90ULL); EXPECT_EQ(fees.developmentFeeBasisPoints, 110ULL); From 771bb5d5e740dc17ff4e0d61e05fcb4326d41a53 Mon Sep 17 00:00:00 2001 From: N-010 Date: Sat, 18 Jul 2026 20:29:36 +0300 Subject: [PATCH 53/59] Update contract state management and introduce old state data structures --- src/contract_core/contract_def.h | 5 +- src/contracts/Nostromo.h | 124 +++++++++++++++++++++++-------- 2 files changed, 97 insertions(+), 32 deletions(-) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 27b42be18..8d19925e2 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -577,8 +577,9 @@ 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 const ContractStateChangeInfo* contractStateChangeInfos = nullptr; - constexpr unsigned int contractStateChangeCount = 0; +// TODO: delete this comment and set the required epoch, after Proposal in NOST + constexpr ContractStateChangeInfo contractStateChangeInfos[] = {{NOST_CONTRACT_INDEX, MIGRATE, 226}}; + constexpr unsigned int contractStateChangeCount = sizeof(contractStateChangeInfos) / sizeof(contractStateChangeInfos[0]); // Class for registering and looking up user procedures independently of input type, for example for notifications diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 0bb6879e0..47f7ca4d4 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -116,6 +116,12 @@ 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 { }; @@ -354,6 +360,68 @@ struct NOST : public ContractBase uint64 allowedBidderWalletCount; }; + struct OldStateData + { + struct investInfo + { + uint64 investedAmount; + uint64 claimedAmount; + uint32 indexOfFundraising; + }; + + struct projectInfo + { + id creator; + uint64 tokenName; + uint64 supplyOfToken; + uint32 startDate; + uint32 endDate; + uint32 numberOfYes; + uint32 numberOfNo; + bit isCreatedFundarasing; + }; + + struct fundaraisingInfo + { + uint64 tokenPrice; + uint64 soldAmount; + uint64 requiredFunds; + uint64 raisedFunds; + uint32 indexOfProject; + uint32 firstPhaseStartDate; + uint32 firstPhaseEndDate; + uint32 secondPhaseStartDate; + uint32 secondPhaseEndDate; + uint32 thirdPhaseStartDate; + uint32 thirdPhaseEndDate; + uint32 listingStartDate; + uint32 cliffEndDate; + uint32 vestingEndDate; + uint8 threshold; + uint8 TGE; + uint8 stepOfVesting; + bit isCreatedToken; + }; + + HashMap users; + HashMap, NOSTROMO_MAX_USER_OLD> voteStatus; + HashMap numberOfVotedProject; + HashSet tokens; + + HashMap, NOSTROMO_MAX_USER_OLD> investors; + HashMap numberOfInvestedProjects; + Array tmpInvestedList; + + Array projects; + + Array fundaraisings; + + id teamAddress; + sint64 transferRightsFee; + uint64 epochRevenue, totalPoolWeight; + uint32 numberOfRegister, numberOfCreatedProject, numberOfFundraising; + }; + struct StateData { /** @brief Configured fee charged when creating a private auction. */ @@ -1980,6 +2048,32 @@ struct NOST : public ContractBase _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. */ @@ -1994,36 +2088,6 @@ struct NOST : public ContractBase */ BEGIN_EPOCH_WITH_LOCALS() { - // TODO: Change to valid epoch - if (qpi.epoch() == NOST_REINITIALIZATION_EPOCH) - { - // Initialize - 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); - } - // 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); if (interContractCallError == NoCallError) From 3d29018bddb920479fdd2d900d6f3b9b9afe6e28 Mon Sep 17 00:00:00 2001 From: N-010 Date: Fri, 7 Aug 2026 00:55:28 +0300 Subject: [PATCH 54/59] =?UTF-8?q?=E2=80=A2=20feat(nostromo):=20harden=20au?= =?UTF-8?q?ction=20lifecycle=20and=20payout=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - retain full closed-auction snapshots and participant history - release active storage after auction finalization or cancellation - queue failed QU payouts and retry them automatically at END_EPOCH - add payout capacity guards and replace magic numbers with constexpr values - optimize retained-auction getters and batch availability lookup - extend contract statistics, error handling, documentation, and tests --- src/contracts/Nostromo.h | 1457 +++++++++++++++++++++++++++++++----- test/contract_nostromo.cpp | 349 ++++++++- 2 files changed, 1566 insertions(+), 240 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 47f7ca4d4..94c9654bb 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -10,18 +10,40 @@ namespace QPI // Maximum number of active auction records stored by the contract, in auctions. constexpr uint64 NOST_AUCTION_NUM = 2048; -// Number of closed auction indices retained in the history ring buffer, in entries. +// 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 revenue distribution may require for management, development, coordinator, and seller. +constexpr uint64 NOST_AUCTION_REVENUE_MAX_PAYOUT_RECIPIENTS = 4; +// Maximum pending-payout slots one service-fee distribution may require for management, development, and coordinator. +constexpr uint64 NOST_AUCTION_SERVICE_FEE_MAX_PAYOUT_RECIPIENTS = 3; +// 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. @@ -142,6 +164,34 @@ struct NOST : public ContractBase SetEmergencyPause = 10 }; + /** @brief Stable public-function identifiers used by the contract ABI. */ + enum class EFunctionId : uint16 + { + 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 + }; + enum class EAuctionType : uint8 { None, @@ -181,7 +231,9 @@ struct NOST : public ContractBase PrivateAuctionAccessDenied, AuctionPaused, AuctionIndexExhausted, - QuantityUnavailable + QuantityUnavailable, + AuctionHasAcceptedBid, + PayoutQueueFull }; /** @@ -490,14 +542,30 @@ struct NOST : public ContractBase /** @brief Total number of auctions ever created; also the next auction index. */ uint64 totalAuctionsCreated; - /** @brief Circular buffer with indices of finalized and cancelled auctions. */ - Array closedAuctionHistory; + /** @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 Auction creation and Batch bid service fees accumulated during the epoch, distributed at `END_EPOCH`. */ uint64 pendingServiceFeePool; @@ -817,6 +885,19 @@ struct NOST : public ContractBase uint64 pendingServiceFeePool; }; + /** @brief Input used to inspect a wallet's registered QU payout. */ + struct GetPendingPayout_input + { + /** @brief Wallet whose unpaid QU amount should be returned. */ + id account; + }; + + struct GetPendingPayout_output + { + /** @brief QU currently owed to the requested wallet. */ + uint64 amount; + }; + /** @brief Input payload used to read the current state of the execution fee reserve guard. */ using GetFeeReserveGuardState_input = NoData; @@ -1046,6 +1127,9 @@ struct NOST : public ContractBase uint64 closedAuctionHistoryCounter; uint64 auctionShareholderDividendPool; uint64 pendingServiceFeePool; + uint64 totalPendingQuPayouts; + uint64 retainedClosedAuctionCount; + uint64 retainedParticipantHistoryCount; uint32 qxTransferFee; uint8 routeAllFeesToDevelopment; uint8 isAuctionTimerPaused; @@ -1286,9 +1370,109 @@ struct NOST : public ContractBase 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; @@ -1302,23 +1486,69 @@ struct NOST : public ContractBase 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; - using GetAuctionSummaries_locals = GetterScan_locals; - using GetActiveAuctionIndices_locals = GetterScan_locals; - using GetAuctionsBySeller_locals = GetterScan_locals; - using GetAuctionByMetadataCid_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; - using GetAuctionCountBySeller_locals = GetterScan_locals; + + struct GetAuctionCountBySeller_locals + { + CountRetainedAuctionsBySeller_input countAuctionsInput; + CountRetainedAuctionsBySeller_output countAuctionsOutput; + }; + using GetAuctionAtCreationSnapshot_locals = GetterScan_locals; struct GetAuctionParticipant_locals @@ -1329,6 +1559,12 @@ struct NOST : public ContractBase 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 { @@ -1349,6 +1585,7 @@ struct NOST : public ContractBase uint64 outputPrice; uint64 priorityQuantity; uint64 salePriorityQuantity; + uint64 effectiveCoverageQuantity; uint64 participantIndex; uint8 lowestWinningPriceFound; }; @@ -1356,6 +1593,8 @@ struct NOST : public ContractBase 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. */ @@ -1492,6 +1731,9 @@ struct NOST : public ContractBase /** @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; }; @@ -1546,6 +1788,96 @@ struct NOST : public ContractBase 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 { @@ -1599,10 +1931,15 @@ struct NOST : public ContractBase ComputeBatchBidAvailability_output computeBatchBidAvailabilityOutput; RecomputeBatchHighestBid_input recomputeBatchHighestBidInput; RecomputeBatchHighestBid_output recomputeBatchHighestBidOutput; + ArchiveParticipant_input archiveParticipantInput; + ArchiveParticipant_output archiveParticipantOutput; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; uint64 activeQuantity; uint64 displacedQuantity; uint64 displacedRefund; uint64 excessQuantity; + uint64 remainingWorstQuantity; CalculateBatchAuctionBidFee_output bidFeeCalculation; uint64 participantIndex; uint64 freeParticipantSlotIndex; @@ -1660,6 +1997,10 @@ struct NOST : public ContractBase 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; @@ -1754,6 +2095,19 @@ struct NOST : public ContractBase 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; @@ -1762,6 +2116,12 @@ struct NOST : public ContractBase 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; @@ -1784,6 +2144,12 @@ struct NOST : public ContractBase 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; @@ -1792,6 +2158,8 @@ struct NOST : public ContractBase struct DistributeAuctionRevenue_locals { AuctionRevenueBreakdown auctionRevenueBreakdown; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; uint64 distributedDividendAmount; uint64 dividendPerShare; }; @@ -1799,6 +2167,8 @@ struct NOST : public ContractBase struct DistributeAuctionServiceFee_locals { AuctionServiceFeeBreakdown auctionServiceFeeBreakdown; + QueueAndFlushQuPayout_input payoutInput; + QueueAndFlushQuPayout_output payoutOutput; uint64 distributedDividendAmount; uint64 dividendPerShare; }; @@ -1809,6 +2179,12 @@ struct NOST : public ContractBase 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; }; @@ -1847,6 +2223,8 @@ struct NOST : public ContractBase struct PlaceBid_locals { AuctionData auction; + FindAuction_input findAuctionInput; + FindAuction_output findAuctionOutput; NostromoProcedureLog log; IsAuctionInteractionPaused_input isAuctionInteractionPausedInput; IsAuctionInteractionPaused_output isAuctionInteractionPausedOutput; @@ -1864,12 +2242,16 @@ struct NOST : public ContractBase struct CancelAuction_locals { AuctionData auction; + FindAuction_input findAuctionInput; + FindAuction_output findAuctionOutput; AuctionParticipantData participantData; NostromoProcedureLog log; RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; DistributeAuctionServiceFee_input distributeAuctionServiceFeeInput; DistributeAuctionServiceFee_output distributeAuctionServiceFeeOutput; + ArchiveClosedAuction_input archiveClosedAuctionInput; + ArchiveClosedAuction_output archiveClosedAuctionOutput; DateAndTime currentDate; uint64 cancellationBaseAmount; uint64 participantIndex; @@ -1878,6 +2260,8 @@ struct NOST : public ContractBase struct ResolvePendingStandardAuction_locals { AuctionData auction; + FindAuction_input findAuctionInput; + FindAuction_output findAuctionOutput; DateAndTime currentDate; NostromoProcedureLog log; @@ -1918,6 +2302,8 @@ struct NOST : public ContractBase { DistributeAuctionServiceFee_input distributeAuctionServiceFeeInput; DistributeAuctionServiceFee_output distributeAuctionServiceFeeOutput; + ProcessPendingQuPayouts_input processPendingQuPayoutsInput; + ProcessPendingQuPayouts_output processPendingQuPayoutsOutput; }; /** @brief Input payload used to move share management rights to another managing contract. */ @@ -1991,28 +2377,29 @@ struct NOST : public ContractBase REGISTER_USER_PROCEDURE(SetFeeReserveGuardConfig, static_cast(EProcedureId::SetFeeReserveGuardConfig)); REGISTER_USER_PROCEDURE(SetEmergencyPause, static_cast(EProcedureId::SetEmergencyPause)); - REGISTER_USER_FUNCTION(GetAuctionByIndex, 1); - REGISTER_USER_FUNCTION(GetAuctionParticipant, 2); - REGISTER_USER_FUNCTION(GetTicksBeforeAuctionLaunch, 3); - REGISTER_USER_FUNCTION(GetAuctionFees, 4); - REGISTER_USER_FUNCTION(GetFeeRecipients, 5); - REGISTER_USER_FUNCTION(GetClosedAuctionHistory, 6); - REGISTER_USER_FUNCTION(GetRouteAllFeesToDevelopment, 7); - REGISTER_USER_FUNCTION(GetContractStats, 8); - REGISTER_USER_FUNCTION(GetAuctionSummaries, 9); - REGISTER_USER_FUNCTION(GetActiveAuctionIndices, 10); - REGISTER_USER_FUNCTION(GetAuctionsBySeller, 11); - REGISTER_USER_FUNCTION(GetAuctionByMetadataCid, 12); - REGISTER_USER_FUNCTION(GetAuctionSummariesByIndexBatch, 13); - REGISTER_USER_FUNCTION(GetAuctionParticipants, 14); - REGISTER_USER_FUNCTION(GetUserParticipations, 15); - REGISTER_USER_FUNCTION(GetLatestAuctionIndex, 16); - REGISTER_USER_FUNCTION(GetAuctionCountBySeller, 17); - REGISTER_USER_FUNCTION(GetAuctionAtCreationSnapshot, 18); - REGISTER_USER_FUNCTION(GetBatchAuctionBidAvailability, 19); - REGISTER_USER_FUNCTION(CalculateBatchAuctionBidFee, 20); - REGISTER_USER_FUNCTION(GetPendingServiceFeePool, 21); - REGISTER_USER_FUNCTION(GetFeeReserveGuardState, 22); + 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)); } /** @@ -2090,6 +2477,7 @@ struct NOST : public ContractBase { // 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; @@ -2116,19 +2504,26 @@ struct NOST : public ContractBase } /** - * @brief Distributes pending service fees and performs auction storage cleanup. + * @brief Retries pending QU payouts, distributes pending service fees, and performs storage cleanup. */ END_EPOCH_WITH_LOCALS() { + CALL(ProcessPendingQuPayouts, locals.processPendingQuPayoutsInput, locals.processPendingQuPayoutsOutput); + // Service fees collected during the epoch are distributed as one batch to avoid repeated dividend dust handling. if (state.get().pendingServiceFeePool > 0) { locals.distributeAuctionServiceFeeInput.feeAmount = state.get().pendingServiceFeePool; CALL(DistributeAuctionServiceFee, locals.distributeAuctionServiceFeeInput, locals.distributeAuctionServiceFeeOutput); - state.mut().pendingServiceFeePool = 0; + // Clear the pool only after all liabilities were registered, so a capacity failure can be retried next epoch. + if (locals.distributeAuctionServiceFeeOutput.success) + { + state.mut().pendingServiceFeePool = 0; + } } state.mut().auctionList.cleanupIfNeeded(); + state.mut().pendingQuPayouts.cleanupIfNeeded(); } /** @@ -2143,11 +2538,13 @@ struct NOST : public ContractBase 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; } + // Subsequent observations either trigger the guard or roll the baseline into a new window. else { diffDateInSecond(state.get().feeReserveBaselineAt, locals.currentDate, locals.guardElapsedSeconds); @@ -2179,6 +2576,7 @@ struct NOST : public ContractBase } CALL(SyncAuctionPauseState, locals.syncAuctionPauseStateInput, locals.syncAuctionPauseStateOutput); + // Lifecycle transitions must not advance while SyncAuctionPauseState still owns the global timer freeze. if (state.get().isAuctionTimerPaused) { return; @@ -2193,6 +2591,7 @@ struct NOST : public ContractBase { 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) @@ -2203,12 +2602,14 @@ struct NOST : public ContractBase 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); } + // A funded bid below the seller's sale price requires an explicit, time-bounded seller choice. else { // Below-sale standard bids enter a seller decision window instead of settling immediately. @@ -2226,6 +2627,7 @@ struct NOST : public ContractBase 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; @@ -2258,9 +2660,11 @@ struct NOST : public ContractBase 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) @@ -2474,14 +2878,366 @@ struct NOST : public ContractBase { 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)); + + 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; + } + } + // First-time recipients consume a new map slot; failure leaves the global liability total unchanged. + else + { + 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; + } + } } /** @@ -2498,6 +3254,11 @@ struct NOST : public ContractBase output.success = 1; return; } + // Reserve worst-case recipient headroom before any fee liability is queued, keeping failure atomic. + 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; @@ -2507,27 +3268,56 @@ struct NOST : public ContractBase { if (input.grossAmount > output.sellerPayout) { - qpi.transfer(state.get().development, input.grossAmount - output.sellerPayout); + locals.payoutInput.recipient = state.get().development; + locals.payoutInput.amount = input.grossAmount - output.sellerPayout; + locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } } } + // Normal routing preserves each configured recipient and accumulates the shareholder portion separately. else { - // Dividend dust stays pooled until it can be distributed evenly to all computors. - state.mut().auctionShareholderDividendPool = - sadd(state.get().auctionShareholderDividendPool, locals.auctionRevenueBreakdown.shareholderDividendAmount); if (locals.auctionRevenueBreakdown.managementFeeAmount > 0) { - qpi.transfer(state.get().management, locals.auctionRevenueBreakdown.managementFeeAmount); + locals.payoutInput.recipient = state.get().management; + locals.payoutInput.amount = locals.auctionRevenueBreakdown.managementFeeAmount; + locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } } if (locals.auctionRevenueBreakdown.developmentFeeAmount > 0) { - qpi.transfer(state.get().development, locals.auctionRevenueBreakdown.developmentFeeAmount); + locals.payoutInput.recipient = state.get().development; + locals.payoutInput.amount = locals.auctionRevenueBreakdown.developmentFeeAmount; + locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } } if (locals.auctionRevenueBreakdown.takeoverCoordinatorFeeAmount > 0) { - qpi.transfer(state.get().takeoverCoordinator, locals.auctionRevenueBreakdown.takeoverCoordinatorFeeAmount); + locals.payoutInput.recipient = state.get().takeoverCoordinator; + locals.payoutInput.amount = locals.auctionRevenueBreakdown.takeoverCoordinatorFeeAmount; + locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } } + // Dividend dust stays pooled until it can be distributed evenly to all computors. + state.mut().auctionShareholderDividendPool = + sadd(state.get().auctionShareholderDividendPool, locals.auctionRevenueBreakdown.shareholderDividendAmount); locals.dividendPerShare = div(state.get().auctionShareholderDividendPool, NUMBER_OF_COMPUTORS); if (locals.dividendPerShare > 0 && qpi.distributeDividends(locals.dividendPerShare)) { @@ -2536,6 +3326,15 @@ struct NOST : public ContractBase } } + 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; + } + output.success = 1; } @@ -2552,31 +3351,60 @@ struct NOST : public ContractBase output.success = 1; return; } + // Management, development, and coordinator may each require a new liability slot. + if (state.get().pendingQuPayouts.population() > state.get().pendingQuPayouts.capacity() - NOST_AUCTION_SERVICE_FEE_MAX_PAYOUT_RECIPIENTS) + { + return; + } // Service fees use fixed recipients unless the runtime override sends all fees to development. if (routeAllFeesToDevelopment(state)) { - qpi.transfer(state.get().development, input.feeAmount); - output.success = 1; + locals.payoutInput.recipient = state.get().development; + locals.payoutInput.amount = input.feeAmount; + locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + output.success = locals.payoutOutput.success; return; } calculateAuctionServiceFeeBreakdown(input.feeAmount, locals.auctionServiceFeeBreakdown); - state.mut().auctionShareholderDividendPool = - sadd(state.get().auctionShareholderDividendPool, locals.auctionServiceFeeBreakdown.shareholderDividendAmount); if (locals.auctionServiceFeeBreakdown.managementFeeAmount > 0) { - qpi.transfer(state.get().management, locals.auctionServiceFeeBreakdown.managementFeeAmount); + locals.payoutInput.recipient = state.get().management; + locals.payoutInput.amount = locals.auctionServiceFeeBreakdown.managementFeeAmount; + locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } } if (locals.auctionServiceFeeBreakdown.developmentFeeAmount > 0) { - qpi.transfer(state.get().development, locals.auctionServiceFeeBreakdown.developmentFeeAmount); + locals.payoutInput.recipient = state.get().development; + locals.payoutInput.amount = locals.auctionServiceFeeBreakdown.developmentFeeAmount; + locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } } if (locals.auctionServiceFeeBreakdown.takeoverCoordinatorFeeAmount > 0) { - qpi.transfer(state.get().takeoverCoordinator, locals.auctionServiceFeeBreakdown.takeoverCoordinatorFeeAmount); + locals.payoutInput.recipient = state.get().takeoverCoordinator; + locals.payoutInput.amount = locals.auctionServiceFeeBreakdown.takeoverCoordinatorFeeAmount; + locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; + CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); + if (!locals.payoutOutput.success) + { + return; + } } + state.mut().auctionShareholderDividendPool = + sadd(state.get().auctionShareholderDividendPool, locals.auctionServiceFeeBreakdown.shareholderDividendAmount); locals.dividendPerShare = div(state.get().auctionShareholderDividendPool, NUMBER_OF_COMPUTORS); if (locals.dividendPerShare > 0 && qpi.distributeDividends(locals.dividendPerShare)) { @@ -2695,9 +3523,7 @@ struct NOST : public ContractBase if (!locals.bestParticipantFound || locals.participantData.bidAmount > locals.bestParticipantData.bidAmount || (locals.participantData.bidAmount == locals.bestParticipantData.bidAmount && - (locals.participantData.lastBidTime < locals.bestParticipantData.lastBidTime || - (locals.participantData.lastBidTime == locals.bestParticipantData.lastBidTime && - locals.participantData.bidIndex < locals.bestParticipantData.bidIndex)))) + locals.participantData.bidIndex < locals.bestParticipantData.bidIndex)) { locals.bestParticipantFound = 1; locals.bestParticipantData = locals.participantData; @@ -2739,6 +3565,7 @@ struct NOST : public ContractBase 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; @@ -2777,10 +3604,14 @@ struct NOST : public ContractBase } } - if (locals.salePriorityQuantity >= locals.auction.core.quantityForSale) + 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; } + // Otherwise expose the full unreserved quantity; the minimum check below decides whether bidding remains viable. else { output.availableQuantity = locals.auction.core.quantityForSale - locals.salePriorityQuantity; @@ -2792,6 +3623,7 @@ struct NOST : public ContractBase output.minimumBidPrice = locals.auction.core.salePrice; output.isAcceptingBids = 1; } + // A full book can still accept a strictly better bid that displaces the current lowest-priced allocation. else { output.availableQuantity = 0; @@ -2836,6 +3668,7 @@ struct NOST : public ContractBase } } + // 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; @@ -2867,6 +3700,14 @@ struct NOST : public ContractBase 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) @@ -2906,7 +3747,7 @@ struct NOST : public ContractBase return; } - // Batch bids always consume a new participant slot; historical slots remain readable after settlement. + // 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) { @@ -2964,9 +3805,11 @@ struct NOST : public ContractBase } } + // 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); @@ -2982,9 +3825,7 @@ struct NOST : public ContractBase if (!locals.worstParticipantFound || locals.participantData.bidAmount < locals.worstParticipantData.bidAmount || (locals.participantData.bidAmount == locals.worstParticipantData.bidAmount && - (locals.participantData.lastBidTime > locals.worstParticipantData.lastBidTime || - (locals.participantData.lastBidTime == locals.worstParticipantData.lastBidTime && - locals.participantData.bidIndex > locals.worstParticipantData.bidIndex)))) + locals.participantData.bidIndex > locals.worstParticipantData.bidIndex)) { locals.worstParticipantFound = 1; locals.worstParticipantData = locals.participantData; @@ -3000,6 +3841,14 @@ struct NOST : public ContractBase 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; @@ -3015,13 +3864,23 @@ struct NOST : public ContractBase locals.worstParticipantData.isWinningBid = 1; } - state.mut().participants.set(locals.worstParticipantSlotIndex, locals.worstParticipantData); if (locals.displacedRefund > 0) { - qpi.transfer(locals.worstParticipantData.participant, locals.displacedRefund); + 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; @@ -3035,7 +3894,10 @@ struct NOST : public ContractBase if (static_cast(qpi.invocationReward()) > locals.bidFeeCalculation.requiredReward) { - qpi.transfer(qpi.invocator(), 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); } @@ -3069,6 +3931,13 @@ struct NOST : public ContractBase 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) @@ -3149,12 +4018,18 @@ struct NOST : public ContractBase } if (locals.highestBidderExists && locals.previousHighestBidderData.participant != qpi.invocator()) { - qpi.transfer(locals.previousHighestBidderData.participant, locals.previousHighestBidderData.escrowedAmount); + 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); } @@ -3181,12 +4056,18 @@ struct NOST : public ContractBase // Refund replaced self-escrow and excess reward after the new bid state is durable. if (locals.previousEscrow > 0) { - qpi.transfer(qpi.invocator(), locals.previousEscrow); + 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) { - qpi.transfer(qpi.invocator(), 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); } @@ -3313,6 +4194,11 @@ struct NOST : public ContractBase { 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) @@ -3329,14 +4215,14 @@ struct NOST : public ContractBase return; } - // Bids were valid when submitted; final fragments may be smaller than `minimumPurchaseQuantity` after displacement. + // Stop before producing a fragment below the auction minimum; the remainder stays with the seller. locals.remainingQuantity = locals.auction.core.quantityForSale; - while (locals.remainingQuantity > 0) + while (locals.remainingQuantity >= locals.auction.core.minimumPurchaseQuantity) { locals.bestParticipantFound = 0; locals.participantIndex = 0; - // Scan all bids for this auction to find the highest price, using earlier bid time as the tie-breaker. + // 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); @@ -3346,9 +4232,7 @@ struct NOST : public ContractBase { if (!locals.bestParticipantFound || locals.participantData.bidAmount > locals.bestParticipantData.bidAmount || (locals.participantData.bidAmount == locals.bestParticipantData.bidAmount && - (locals.participantData.lastBidTime < locals.bestParticipantData.lastBidTime || - (locals.participantData.lastBidTime == locals.bestParticipantData.lastBidTime && - locals.participantData.bidIndex < locals.bestParticipantData.bidIndex)))) + locals.participantData.bidIndex < locals.bestParticipantData.bidIndex)) { locals.bestParticipantFound = 1; locals.bestParticipantData = locals.participantData; @@ -3388,12 +4272,22 @@ struct NOST : public ContractBase // Return the unused part of the winner escrow when the participant requested more than the remaining supply. if (locals.refundAmount > 0) { - qpi.transfer(locals.bestParticipantData.participant, locals.refundAmount); + 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; + } } - // Clear the processed escrow so the same bid cannot participate in later iterations. + // 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); } @@ -3406,12 +4300,22 @@ struct NOST : public ContractBase { if (locals.participantData.escrowedAmount > 0) { - qpi.transfer(locals.participantData.participant, locals.participantData.escrowedAmount); + 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; @@ -3425,11 +4329,12 @@ struct NOST : public ContractBase } // 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.sellerPayout > 0) + if (!locals.distributeAuctionRevenueOutput.success) { - qpi.transfer(locals.auction.core.seller, locals.distributeAuctionRevenueOutput.sellerPayout); + return; } // Persist the final sold quantity and close the auction as settled. @@ -3437,8 +4342,9 @@ struct NOST : public ContractBase locals.auction.core.status = EAuctionStatus::Finalized; locals.auction.core.settledAt = input.currentDate; locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; - state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); - addClosedAuctionToHistory(state, locals.auction.core.auctionIndex); + state.mut().totalFinalizedAuctions = sadd(state.get().totalFinalizedAuctions, 1ULL); + locals.archiveClosedAuctionInput.auction = locals.auction; + CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); output.success = 1; } @@ -3460,6 +4366,10 @@ struct NOST : public ContractBase { 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()) @@ -3476,17 +4386,21 @@ struct NOST : public ContractBase 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.sellerPayout > 0) + if (!locals.distributeAuctionRevenueOutput.success) { - qpi.transfer(locals.auction.core.seller, locals.distributeAuctionRevenueOutput.sellerPayout); + 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; @@ -3511,8 +4425,9 @@ struct NOST : public ContractBase locals.auction.core.highestBidder = NULL_ID; } locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; - state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); - addClosedAuctionToHistory(state, locals.auction.core.auctionIndex); + state.mut().totalFinalizedAuctions = sadd(state.get().totalFinalizedAuctions, 1ULL); + locals.archiveClosedAuctionInput.auction = locals.auction; + CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); output.success = 1; } @@ -3534,6 +4449,10 @@ struct NOST : public ContractBase { return; } + if (state.get().pendingQuPayouts.population() == state.get().pendingQuPayouts.capacity()) + { + return; + } locals.highestBidderSlotIndex = locals.auction.core.highestBidSlotIndex; if (locals.highestBidderSlotIndex < state.get().participants.capacity()) @@ -3546,12 +4465,22 @@ struct NOST : public ContractBase // Seller rejection unwinds the pending bid instead of distributing its escrow as proceeds. if (locals.highestBidderExists && locals.highestBidderData.escrowedAmount > 0) { - qpi.transfer(locals.highestBidderData.participant, locals.highestBidderData.escrowedAmount); + 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); } @@ -3568,8 +4497,9 @@ struct NOST : public ContractBase locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; locals.auction.core.status = EAuctionStatus::Finalized; locals.auction.core.settledAt = input.currentDate; - state.mut().auctionList.replace(locals.auction.core.auctionIndex, locals.auction); - addClosedAuctionToHistory(state, locals.auction.core.auctionIndex); + state.mut().totalFinalizedAuctions = sadd(state.get().totalFinalizedAuctions, 1ULL); + locals.archiveClosedAuctionInput.auction = locals.auction; + CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); output.success = 1; } @@ -3939,11 +4869,13 @@ struct NOST : public ContractBase 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()); } - output.errorCode = EAuctionError::AuctionNotFound; + 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; @@ -4106,11 +5038,13 @@ struct NOST : public ContractBase 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()); } - output.errorCode = EAuctionError::AuctionNotFound; + 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); @@ -4143,14 +5077,39 @@ struct NOST : public ContractBase return; } + if (locals.auction.core.nextBidIndex != 0) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::AuctionHasAcceptedBid; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); + logProcedureResult(locals.log); + return; + } + + if (state.get().pendingQuPayouts.population() > state.get().pendingQuPayouts.capacity() - NOST_AUCTION_SERVICE_FEE_MAX_PAYOUT_RECIPIENTS) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + output.errorCode = EAuctionError::PayoutQueueFull; + setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, + output.cancellationFee); + logProcedureResult(locals.log); + return; + } + // The fee base represents the full reserve value of the lot being withdrawn. locals.cancellationBaseAmount = locals.auction.core.salePrice; if (locals.auction.core.type == EAuctionType::Batch) { locals.cancellationBaseAmount = smul(locals.auction.core.salePrice, locals.auction.core.quantityForSale); } - output.cancellationFee = - div(smul(locals.cancellationBaseAmount, state.get().auctionCancellationFeeBasisPoints), NOST_BASIS_POINTS_SCALE); + output.cancellationFee = calculateBasisPointAmount(locals.cancellationBaseAmount, state.get().auctionCancellationFeeBasisPoints); if (static_cast(qpi.invocationReward()) < output.cancellationFee) { @@ -4165,25 +5124,6 @@ struct NOST : public ContractBase return; } - // Clear any participant escrow defensively before returning the seller's lot. - 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) - { - qpi.transfer(locals.participantData.participant, locals.participantData.escrowedAmount); - output.refundedAmount = sadd(output.refundedAmount, locals.participantData.escrowedAmount); - } - locals.participantData = {}; - state.mut().participants.set(locals.participantIndex, locals.participantData); - } - - ++locals.participantIndex; - } - locals.rollbackAuctionLotAssetsInput.auctionLotItems = locals.auction.core.auctionLotItems; locals.rollbackAuctionLotAssetsInput.recipient = locals.auction.core.seller; CALL(RollbackAuctionLotAssets, locals.rollbackAuctionLotAssetsInput, locals.rollbackAuctionLotAssetsOutput); @@ -4198,8 +5138,9 @@ struct NOST : public ContractBase locals.auction.core.highestBidQuantity = 0; locals.auction.core.highestBidder = NULL_ID; locals.auction.core.highestBidSlotIndex = NOST_INVALID_PARTICIPANT_SLOT; - state.mut().auctionList.replace(input.auctionIndex, locals.auction); - addClosedAuctionToHistory(state, locals.auction.core.auctionIndex); + state.mut().totalCancelledAuctions = sadd(state.get().totalCancelledAuctions, 1ULL); + locals.archiveClosedAuctionInput.auction = locals.auction; + CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); // Cancellation fees are distributed immediately because cancellation is already a settlement action. locals.distributeAuctionServiceFeeInput.feeAmount = output.cancellationFee; @@ -4251,7 +5192,9 @@ struct NOST : public ContractBase if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) { - output.errorCode = EAuctionError::AuctionNotFound; + 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); @@ -4528,10 +5471,13 @@ struct NOST : public ContractBase PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionByIndex) { output.found = 0; - if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + 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.auction.core = locals.auction.core; @@ -4577,9 +5523,7 @@ struct NOST : public ContractBase continue; } - if (!locals.bestParticipantFound || locals.participantData.lastBidTime > output.participantData.lastBidTime || - (locals.participantData.lastBidTime == output.participantData.lastBidTime && - locals.participantData.bidIndex > output.participantData.bidIndex)) + if (!locals.bestParticipantFound || locals.participantData.bidIndex > output.participantData.bidIndex) { locals.bestParticipantFound = 1; locals.bestParticipantSlotIndex = locals.participantSlotIndex; @@ -4587,6 +5531,22 @@ struct NOST : public ContractBase output.found = 1; } } + // 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) + { + locals.participantData = state.get().participantHistory.get(locals.participantSlotIndex); + if (!locals.participantData.isUsed || locals.participantData.auctionIndex != input.auctionIndex || + locals.participantData.participant != input.participant) + { + continue; + } + if (!locals.bestParticipantFound || locals.participantData.bidIndex > output.participantData.bidIndex) + { + locals.bestParticipantFound = 1; + output.participantData = locals.participantData; + output.found = 1; + } + } } /** @@ -4651,9 +5611,17 @@ struct NOST : public ContractBase * @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(GetClosedAuctionHistory) + PUBLIC_FUNCTION_WITH_LOCALS(GetClosedAuctionHistory) { - output.auctionIndices = state.get().closedAuctionHistory; + // 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; } @@ -4667,6 +5635,13 @@ struct NOST : public ContractBase */ PUBLIC_FUNCTION(GetPendingServiceFeePool) { output.pendingServiceFeePool = state.get().pendingServiceFeePool; } + /** @brief Returns the QU obligation currently registered for one wallet. */ + PUBLIC_FUNCTION(GetPendingPayout) + { + output.amount = 0; + state.get().pendingQuPayouts.get(input.account, output.amount); + } + /** * @brief Returns the current state of the execution fee reserve guard, including a live reserve reading. */ @@ -4690,6 +5665,11 @@ struct NOST : public ContractBase output.stats.closedAuctionHistoryCounter = state.get().closedAuctionHistoryCounter; output.stats.auctionShareholderDividendPool = state.get().auctionShareholderDividendPool; output.stats.pendingServiceFeePool = state.get().pendingServiceFeePool; + 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; @@ -4706,20 +5686,16 @@ struct NOST : public ContractBase } } - for (locals.auctionIndex = 0; locals.auctionIndex < state.get().totalAuctionsCreated; ++locals.auctionIndex) + for (locals.auctionElementIndex = state.get().auctionList.nextElementIndex(NULL_INDEX); locals.auctionElementIndex != NULL_INDEX; + locals.auctionElementIndex = state.get().auctionList.nextElementIndex(locals.auctionElementIndex)) { - if (!state.get().auctionList.get(locals.auctionIndex, locals.auction)) - { - continue; - } + 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; - case EAuctionStatus::Finalized: output.stats.finalizedAuctionCount = sadd(output.stats.finalizedAuctionCount, 1ULL); break; - case EAuctionStatus::Cancelled: output.stats.cancelledAuctionCount = sadd(output.stats.cancelledAuctionCount, 1ULL); break; default: break; } } @@ -4730,20 +5706,38 @@ struct NOST : public ContractBase */ PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionSummaries) { - output.totalCount = state.get().totalAuctionsCreated; + // 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); - // This getter paginates by creation index, so skipped missing records still count in `totalCount`. - for (locals.auctionIndex = input.offset; locals.auctionIndex < state.get().totalAuctionsCreated && output.returnedCount < locals.boundedLimit; - ++locals.auctionIndex) + 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().auctionList.get(locals.auctionIndex, locals.auction)) + CALL(SelectNextRetainedAuction, locals.selectNextAuctionInput, locals.selectNextAuctionOutput); + if (!locals.selectNextAuctionOutput.found) { - continue; + 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); } - 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; } } @@ -4752,26 +5746,35 @@ struct NOST : public ContractBase */ PUBLIC_FUNCTION_WITH_LOCALS(GetActiveAuctionIndices) { - output.totalCount = 0; + // 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); - // Filtered getters count matches before pagination so callers can request the next page. - for (locals.auctionIndex = 0; locals.auctionIndex < state.get().totalAuctionsCreated; ++locals.auctionIndex) + if (locals.boundedLimit == 0 || input.offset >= output.totalCount) { - if (!state.get().auctionList.get(locals.auctionIndex, locals.auction)) - { - continue; - } - if (locals.auction.core.status != EAuctionStatus::Active && locals.auction.core.status != EAuctionStatus::PendingSellerDecision) + return; + } + + locals.scannedAuctionCount = 0; + locals.selectNextAuctionInput.hasAfterAuctionIndex = 0; + locals.selectNextAuctionInput.includeClosedAuctions = 0; + locals.selectNextAuctionInput.filterBySeller = 0; + // Select only the requested live-map prefix and page; closed history cannot contain active auctions. + while (output.returnedCount < locals.boundedLimit && locals.scannedAuctionCount < output.totalCount) + { + CALL(SelectNextRetainedAuction, locals.selectNextAuctionInput, locals.selectNextAuctionOutput); + if (!locals.selectNextAuctionOutput.found) { - continue; + break; } - if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) + locals.selectNextAuctionInput.afterAuctionIndex = locals.selectNextAuctionOutput.auction.core.auctionIndex; + locals.selectNextAuctionInput.hasAfterAuctionIndex = 1; + if (locals.scannedAuctionCount >= input.offset) { - output.auctionIndices.set(output.returnedCount, locals.auction.core.auctionIndex); + output.auctionIndices.set(output.returnedCount, locals.selectNextAuctionOutput.auction.core.auctionIndex); output.returnedCount = sadd(output.returnedCount, 1ULL); } - output.totalCount = sadd(output.totalCount, 1ULL); + locals.scannedAuctionCount = sadd(locals.scannedAuctionCount, 1ULL); } } @@ -4780,22 +5783,39 @@ struct NOST : public ContractBase */ PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionsBySeller) { - output.totalCount = 0; 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); - for (locals.auctionIndex = 0; locals.auctionIndex < state.get().totalAuctionsCreated; ++locals.auctionIndex) + if (locals.boundedLimit == 0 || input.offset >= output.totalCount) + { + return; + } + + locals.scannedAuctionCount = 0; + locals.selectNextAuctionInput.seller = input.seller; + locals.selectNextAuctionInput.hasAfterAuctionIndex = 0; + locals.selectNextAuctionInput.includeClosedAuctions = 1; + locals.selectNextAuctionInput.filterBySeller = 1; + // The selector skips other sellers, so only the requested seller's prefix and page are ordered. + while (output.returnedCount < locals.boundedLimit && locals.scannedAuctionCount < output.totalCount) { - if (!state.get().auctionList.get(locals.auctionIndex, locals.auction) || locals.auction.core.seller != input.seller) + CALL(SelectNextRetainedAuction, locals.selectNextAuctionInput, locals.selectNextAuctionOutput); + if (!locals.selectNextAuctionOutput.found) { - continue; + break; } - if (output.totalCount >= input.offset && output.returnedCount < locals.boundedLimit) + locals.auction = locals.selectNextAuctionOutput.auction; + locals.selectNextAuctionInput.afterAuctionIndex = locals.auction.core.auctionIndex; + locals.selectNextAuctionInput.hasAfterAuctionIndex = 1; + if (locals.scannedAuctionCount >= input.offset) { fillAuctionSummary(locals.auction, locals.auctionSummary); output.auctions.set(output.returnedCount, locals.auctionSummary); output.returnedCount = sadd(output.returnedCount, 1ULL); } - output.totalCount = sadd(output.totalCount, 1ULL); + locals.scannedAuctionCount = sadd(locals.scannedAuctionCount, 1ULL); } } @@ -4806,30 +5826,16 @@ struct NOST : public ContractBase { output.found = 0; output.auctionIndex = 0; - // Metadata lookup returns the first matching auction in creation order. - for (locals.auctionIndex = 0; locals.auctionIndex < state.get().totalAuctionsCreated; ++locals.auctionIndex) + 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) { - if (!state.get().auctionList.get(locals.auctionIndex, locals.auction)) - { - continue; - } - locals.metadataMatches = 1; - for (locals.metadataIndex = 0; locals.metadataIndex < NOST_AUCTION_METADATA_CID_LENGTH; ++locals.metadataIndex) - { - if (locals.auction.core.metadataIpfsCid.get(locals.metadataIndex) != input.metadataIpfsCid.get(locals.metadataIndex)) - { - locals.metadataMatches = 0; - break; - } - } - if (locals.metadataMatches) - { - output.found = 1; - output.auctionIndex = locals.auction.core.auctionIndex; - fillAuctionSummary(locals.auction, output.auction); - return; - } + return; } + output.found = 1; + output.auctionIndex = locals.findAuctionOutput.auction.core.auctionIndex; + fillAuctionSummary(locals.findAuctionOutput.auction, output.auction); } /** @@ -4843,8 +5849,11 @@ struct NOST : public ContractBase for (locals.requestedIndex = 0; locals.requestedIndex < locals.boundedLimit; ++locals.requestedIndex) { locals.auctionIndex = input.auctionIndices.get(locals.requestedIndex); - if (state.get().auctionList.get(locals.auctionIndex, locals.auction)) + locals.findAuctionInput.auctionIndex = locals.auctionIndex; + CALL(FindAuction, locals.findAuctionInput, locals.findAuctionOutput); + if (locals.findAuctionOutput.found) { + locals.auction = locals.findAuctionOutput.auction; fillAuctionSummary(locals.auction, locals.auctionSummary); output.auctions.set(locals.requestedIndex, locals.auctionSummary); output.found.set(locals.requestedIndex, 1); @@ -4877,6 +5886,22 @@ struct NOST : public ContractBase } output.totalCount = sadd(output.totalCount, 1ULL); } + // 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) + { + 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) + { + fillParticipantSummary(locals.participantData, locals.participantSummary); + output.participants.set(output.returnedCount, locals.participantSummary); + output.returnedCount = sadd(output.returnedCount, 1ULL); + } + output.totalCount = sadd(output.totalCount, 1ULL); + } } /** @@ -4903,6 +5928,22 @@ struct NOST : public ContractBase } output.totalCount = sadd(output.totalCount, 1ULL); } + // Continue the same page over archived bids after accounting for matching live entries. + for (locals.participantSlotIndex = 0; locals.participantSlotIndex < state.get().participantHistory.capacity(); ++locals.participantSlotIndex) + { + locals.participantData = state.get().participantHistory.get(locals.participantSlotIndex); + if (!locals.participantData.isUsed || locals.participantData.participant != input.participant) + { + 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); + } } /** @@ -4919,14 +5960,9 @@ struct NOST : public ContractBase */ PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionCountBySeller) { - output.count = 0; - for (locals.auctionIndex = 0; locals.auctionIndex < state.get().totalAuctionsCreated; ++locals.auctionIndex) - { - if (state.get().auctionList.get(locals.auctionIndex, locals.auction) && locals.auction.core.seller == input.seller) - { - output.count = sadd(output.count, 1ULL); - } - } + locals.countAuctionsInput.seller = input.seller; + CALL(CountRetainedAuctionsBySeller, locals.countAuctionsInput, locals.countAuctionsOutput); + output.count = locals.countAuctionsOutput.count; } /** @@ -4935,10 +5971,13 @@ struct NOST : public ContractBase PUBLIC_FUNCTION_WITH_LOCALS(GetAuctionAtCreationSnapshot) { output.found = 0; - if (!state.get().auctionList.get(input.auctionIndex, locals.auction)) + 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; @@ -4955,6 +5994,7 @@ struct NOST : public ContractBase /** * @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) @@ -4962,6 +6002,16 @@ struct NOST : public ContractBase locals.computeBatchBidAvailabilityInput.auctionIndex = input.auctionIndex; locals.computeBatchBidAvailabilityInput.bidAmount = 0; CALL(ComputeBatchBidAvailability, locals.computeBatchBidAvailabilityInput, output); + // Live auctions are fully classified by the availability helper, including non-Batch auctions. + if (output.found) + { + return; + } + + // A retained closed auction still exists for lookup purposes, but can never accept another bid. + locals.isClosedAuctionRetainedInput.auctionIndex = input.auctionIndex; + CALL(IsClosedAuctionRetained, locals.isClosedAuctionRetainedInput, locals.isClosedAuctionRetainedOutput); + output.found = locals.isClosedAuctionRetainedOutput.found; } /** @@ -5230,6 +6280,15 @@ struct NOST : public ContractBase return getAuctionShareholderFeeBasisPoints(grossAmount, state.get()); } + /** + * @brief Computes `floor(amount * basisPoints / 10000)` without overflowing the intermediate product. + */ + static uint64 calculateBasisPointAmount(uint64 amount, uint64 basisPoints) + { + return sadd(smul(div(amount, NOST_BASIS_POINTS_SCALE), basisPoints), + div(smul(mod(amount, NOST_BASIS_POINTS_SCALE), basisPoints), NOST_BASIS_POINTS_SCALE)); + } + /** * @brief Computes the exact auction fee split without performing transfers. * @note Keep this helper pure so tests can reuse the same arithmetic as `DistributeAuctionRevenue`. @@ -5239,12 +6298,11 @@ struct NOST : public ContractBase { output.sellerPayout = grossAmount; output.shareholderFeeBasisPoints = getAuctionShareholderFeeBasisPoints(grossAmount, state); - output.shareholderFeeAmount = div(smul(grossAmount, output.shareholderFeeBasisPoints), NOST_BASIS_POINTS_SCALE); - output.shareholderDividendAmount = - div(smul(output.shareholderFeeAmount, state.get().shareholderDividendBasisPoints), NOST_BASIS_POINTS_SCALE); - output.managementFeeAmount = div(smul(grossAmount, state.get().managementFeeBasisPoints), NOST_BASIS_POINTS_SCALE); - output.developmentFeeAmount = div(smul(grossAmount, state.get().developmentFeeBasisPoints), NOST_BASIS_POINTS_SCALE); - output.takeoverCoordinatorBaseAmount = div(smul(grossAmount, state.get().takeoverCoordinatorFeeBasisPoints), NOST_BASIS_POINTS_SCALE); + 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; @@ -5256,14 +6314,14 @@ struct NOST : public ContractBase */ static void calculateAuctionServiceFeeBreakdown(uint64 feeAmount, AuctionServiceFeeBreakdown& output) { - output.shareholderDividendAmount = div(smul(feeAmount, NOST_AUCTION_SERVICE_FEE_SHAREHOLDER_BP), NOST_BASIS_POINTS_SCALE); - output.managementFeeAmount = div(smul(feeAmount, NOST_AUCTION_SERVICE_FEE_MANAGEMENT_BP), NOST_BASIS_POINTS_SCALE); - output.developmentFeeAmount = div(smul(feeAmount, NOST_AUCTION_SERVICE_FEE_DEVELOPMENT_BP), NOST_BASIS_POINTS_SCALE); - output.takeoverCoordinatorFeeAmount = div(smul(feeAmount, NOST_AUCTION_SERVICE_FEE_TAKEOVER_COORDINATOR_BP), NOST_BASIS_POINTS_SCALE); + 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 = - output.shareholderDividendAmount + (feeAmount - output.shareholderDividendAmount - output.managementFeeAmount - - output.developmentFeeAmount - output.takeoverCoordinatorFeeAmount); + sadd(output.shareholderDividendAmount, feeAmount - output.shareholderDividendAmount - output.managementFeeAmount - + output.developmentFeeAmount - output.takeoverCoordinatorFeeAmount); } /** @@ -5324,15 +6382,6 @@ struct NOST : public ContractBase return state.get().routeAllFeesToDevelopment; } - /** - * @brief Appends a closed auction index to the ring-buffer history. - */ - static void addClosedAuctionToHistory(QPI::ContractState& state, uint64 auctionIndex) - { - state.mut().closedAuctionHistory.set(mod(state.get().closedAuctionHistoryCounter, state.get().closedAuctionHistory.capacity()), auctionIndex); - state.mut().closedAuctionHistoryCounter = sadd(state.get().closedAuctionHistoryCounter, 1ULL); - } - /** * @brief Packs year, month, and day into the contract date-stamp format. */ diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index 825a5becd..fa693eddb 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -549,6 +549,15 @@ class ContractTestingNOST : protected ContractTesting 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::SetFeeReserveGuardConfig_output setFeeReserveGuardConfig(const id& caller, uint64 dropBasisPoints, uint64 windowSeconds) { NOST::SetFeeReserveGuardConfig_input input{}; @@ -920,6 +929,78 @@ TEST(ContractNostromoAuction, AuctionIndexAndExpandedGetterSurfaceAuction) 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; @@ -2207,7 +2288,7 @@ TEST(ContractNostromoAuction, BatchMinimumPurchaseTailAndSameBidderDisplacementA 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, 240ULL); + EXPECT_EQ(improved.refundedAmount, 300ULL); const auto participants = nostromo.getAuctionParticipants(createOutput.auctionIndex, 0, 64); ASSERT_EQ(participants.totalCount, 2ULL); @@ -2224,12 +2305,87 @@ TEST(ContractNostromoAuction, BatchMinimumPurchaseTailAndSameBidderDisplacementA quantityAtForty = participants.participants.get(index).requestedQuantity; } } - EXPECT_EQ(quantityAtThirty, 2ULL); + EXPECT_EQ(quantityAtThirty, 0ULL); EXPECT_EQ(quantityAtForty, 8ULL); nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.managedShares(asset, bidderA), 10); - EXPECT_EQ(nostromo.managedShares(asset, seller), 0); + 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) + { + 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) + { + 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); + nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, bidQuantity, bidPrice); + } + + 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); + uint64 allocatedTotal = 0; + for (uint64 participantIndex = 0; participantIndex < participants.returnedCount; ++participantIndex) + { + const auto participant = participants.participants.get(participantIndex); + EXPECT_LE(participant.allocatedQuantity, participant.requestedQuantity); + EXPECT_TRUE(participant.allocatedQuantity == 0 || participant.allocatedQuantity >= minimumPurchaseQuantity); + allocatedTotal += participant.allocatedQuantity; + } + EXPECT_EQ(allocatedTotal, auction.core.allocatedQuantity); + EXPECT_LE(allocatedTotal, quantityForSale); + EXPECT_EQ(nostromo.managedShares(asset, seller), static_cast(quantityForSale - allocatedTotal)); } } @@ -3131,41 +3287,59 @@ TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeR } } -TEST(ContractNostromoAuction, CancelAuctionRefundsBidsAndFreesParticipantSlotsAuction) +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); + { + 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}; - const auto notFound = nostromo.cancelAuction(seller, 800, 10); - EXPECT_EQ(notFound.errorCode, NOST::EAuctionError::AuctionNotFound); + EXPECT_EQ(nostromo.issueAsset(seller, assetName, 2), 2); + EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 2), 2); - const auto forbidden = nostromo.cancelAuction(bidder, createOutput.auctionIndex, 10); - EXPECT_EQ(forbidden.errorCode, NOST::EAuctionError::Forbidden); + 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 insufficient = nostromo.cancelAuction(seller, createOutput.auctionIndex, 1); - EXPECT_EQ(insufficient.errorCode, NOST::EAuctionError::InsufficientFunds); + const auto notFound = nostromo.cancelAuction(seller, 800, 10); + EXPECT_EQ(notFound.errorCode, NOST::EAuctionError::AuctionNotFound); - const auto bidderBalanceBeforeCancel = getBalance(bidder); - const auto success = nostromo.cancelAuction(seller, createOutput.auctionIndex, 2); - EXPECT_EQ(success.errorCode, NOST::EAuctionError::Success); - EXPECT_EQ(success.refundedAmount, 12ULL); - EXPECT_EQ(getBalance(bidder) - bidderBalanceBeforeCancel, 12); - EXPECT_EQ(nostromo.getParticipant(createOutput.auctionIndex, bidder).found, 0); + const auto forbidden = nostromo.cancelAuction(bidder, createOutput.auctionIndex, 10); + EXPECT_EQ(forbidden.errorCode, NOST::EAuctionError::Forbidden); - const auto closed = nostromo.cancelAuction(seller, createOutput.auctionIndex, 2); - EXPECT_EQ(closed.errorCode, NOST::EAuctionError::AuctionClosed); - EXPECT_EQ(nostromo.getAuction(createOutput.auctionIndex).auction.core.status, NOST::EAuctionStatus::Cancelled); - EXPECT_EQ(nostromo.managedShares(asset, seller), 2); + 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) @@ -3198,6 +3372,37 @@ TEST(ContractNostromoAuction, CancelAuctionRejectsInvalidCasesWithoutBidsAuction 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; @@ -3235,15 +3440,20 @@ TEST(ContractNostromoAuction, ClosedAuctionHistoryGetterExposesRingBufferOverwri ContractTestingNOST nostromo; const uint64 overwrittenAuctionIndex = 22000; const uint64 latestAuctionIndex = 23000; + NOST::AuctionData archivedAuction{}; - nostromo.stateData().closedAuctionHistory.set(0, overwrittenAuctionIndex); + 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) { - nostromo.stateData().closedAuctionHistory.set(index, 24000 + index); + archivedAuction.core.auctionIndex = 24000 + index; + nostromo.stateData().closedAuctionHistory.set(index, archivedAuction); ++nostromo.stateData().closedAuctionHistoryCounter; } - nostromo.stateData().closedAuctionHistory.set(0, latestAuctionIndex); + archivedAuction.core.auctionIndex = latestAuctionIndex; + nostromo.stateData().closedAuctionHistory.set(0, archivedAuction); ++nostromo.stateData().closedAuctionHistoryCounter; const auto history = nostromo.getClosedAuctionHistory(); @@ -3251,6 +3461,8 @@ TEST(ContractNostromoAuction, ClosedAuctionHistoryGetterExposesRingBufferOverwri 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) @@ -3340,6 +3552,71 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) 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; + + 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); + NOST::AuctionRevenueBreakdown breakdown{}; + nostromo.calculateAuctionRevenueBreakdown(grossAmount, breakdown); + EXPECT_EQ(breakdown.sellerPayout + breakdown.shareholderDividendAmount + breakdown.managementFeeAmount + + breakdown.developmentFeeAmount + breakdown.takeoverCoordinatorFeeAmount, + grossAmount); + 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(breakdown.sellerPayout, expectedImmediatePayout); + EXPECT_EQ(static_cast(getBalance(seller) - sellerBeforeSettlement), expectedImmediatePayout); + EXPECT_EQ(nostromo.getPendingPayout(seller).amount, breakdown.sellerPayout - expectedImmediatePayout); + EXPECT_EQ(nostromo.getContractStats().stats.totalPendingQuPayouts, breakdown.sellerPayout - expectedImmediatePayout); + + nostromo.endEpoch(); + EXPECT_EQ(static_cast(getBalance(seller) - sellerBeforeSettlement), breakdown.sellerPayout); + 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, ShareholderFeeTiersAreAppliedAuction) { struct TierCase From b392532e2f6cd6ca4912904f0c6ba12e8d6ecb97 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 13 Aug 2026 15:35:37 +0300 Subject: [PATCH 55/59] update private auction access logic to allow combined access modes --- src/contracts/Nostromo.h | 17 +++++-------- test/contract_nostromo.cpp | 52 +++++++++++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 94c9654bb..acccf105a 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -4547,7 +4547,7 @@ struct NOST : public ContractBase * @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 exactly one access mode. + * 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) { @@ -4700,7 +4700,7 @@ struct NOST : public ContractBase return; } - // Private auctions must choose exactly one access gate: wallet list or asset ownership. + // 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; @@ -4919,19 +4919,16 @@ struct NOST : public ContractBase return; } - // Private access accepts either the configured asset gate or the configured wallet gate. + // When both gates are configured, satisfying either one grants access. if (locals.auction.core.visibility == EAuctionVisibility::Private) { - if (locals.auction.requiredAccessAssets.population() > 0) + locals.hasAccess = locals.auction.allowedBidderWallets.population() > 0 && locals.auction.allowedBidderWallets.contains(qpi.invocator()); + if (!locals.hasAccess && locals.auction.requiredAccessAssets.population() > 0) { locals.hasRequiredAccessAssetInput.auctionIndex = input.auctionIndex; CALL(HasRequiredAccessAsset, locals.hasRequiredAccessAssetInput, locals.hasRequiredAccessAssetOutput); locals.hasAccess = locals.hasRequiredAccessAssetOutput.hasRequiredAccessAsset; } - else - { - locals.hasAccess = locals.auction.allowedBidderWallets.contains(qpi.invocator()); - } if (!locals.hasAccess) { @@ -6220,11 +6217,11 @@ struct NOST : public ContractBase } /** - * @brief Validates that private auctions use exactly one supported access mode. + * @brief Validates that private auctions use at least one supported access mode. */ constexpr static bool validatePrivateAuctionAccess(EAuctionVisibility visibility, uint64 requiredAccessAssetCount, uint64 allowedWalletCount) { - return visibility != EAuctionVisibility::Private || ((requiredAccessAssetCount > 0) != (allowedWalletCount > 0)); + return visibility != EAuctionVisibility::Private || requiredAccessAssetCount > 0 || allowedWalletCount > 0; } /** diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index fa693eddb..c3824d77f 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -1913,12 +1913,6 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) privateWithoutGate.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); EXPECT_EQ(nostromo.createAuction(seller, privateWithoutGate, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, NOST::EAuctionError::InvalidInput); - auto privateWithBothGates = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); - privateWithBothGates.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - privateWithBothGates.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({id(99, 1, 1, 1)}); - privateWithBothGates.requiredAccessAssets = ContractTestingNOST::makeRequiredAccessAssets({NOST::AuctionAssetEntry{accessAsset, 1}}); - EXPECT_EQ(nostromo.createAuction(seller, privateWithBothGates, NOST_DEFAULT_PRIVATE_AUCTION_FEE).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}}); @@ -2568,6 +2562,52 @@ TEST(ContractNostromoAuction, PrivateAuctionAccessRulesAuction) } } +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; From ec3cd2c76fa783f9a513d03f050de032bd0bd97f Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 13 Aug 2026 17:46:19 +0300 Subject: [PATCH 56/59] implement shared fee pool management and retrieval functions --- src/contracts/Nostromo.h | 378 ++++++++++++++++++++++--------------- test/contract_nostromo.cpp | 110 ++++++++++- 2 files changed, 331 insertions(+), 157 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index acccf105a..2f03c3023 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -18,10 +18,8 @@ constexpr uint64 NOST_AUCTION_METADATA_CID_LENGTH = 64; 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 revenue distribution may require for management, development, coordinator, and seller. -constexpr uint64 NOST_AUCTION_REVENUE_MAX_PAYOUT_RECIPIENTS = 4; -// Maximum pending-payout slots one service-fee distribution may require for management, development, and coordinator. -constexpr uint64 NOST_AUCTION_SERVICE_FEE_MAX_PAYOUT_RECIPIENTS = 3; +// 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. @@ -66,13 +64,13 @@ constexpr uint64 NOST_DEFAULT_AUCTION_MANAGEMENT_FEE_BP = 50ULL; 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 private-creation and cancellation service fees, in basis points. +// 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 private-creation and cancellation service fees, in basis points. +// 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 private-creation and cancellation service fees, in basis points. +// 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 private-creation and cancellation service fees, in basis points. +// 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; @@ -189,7 +187,8 @@ struct NOST : public ContractBase CalculateBatchAuctionBidFee = 20, GetPendingServiceFeePool = 21, GetFeeReserveGuardState = 22, - GetPendingPayout = 23 + GetPendingPayout = 23, + GetNostromoFeePool = 24 }; enum class EAuctionType : uint8 @@ -474,6 +473,23 @@ struct NOST : public ContractBase uint32 numberOfRegister, numberOfCreatedProject, numberOfFundraising; }; + /** + * @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. */ @@ -485,7 +501,7 @@ struct NOST : public ContractBase /** @brief Configured cancellation fee rate in basis points. */ uint64 auctionCancellationFeeBasisPoints; - /** @brief Undistributed shareholder revenue from auction proceeds and service fees reserved for contract dividends. */ + /** @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. */ @@ -567,8 +583,8 @@ struct NOST : public ContractBase /** @brief Lifetime number of cancelled auctions. */ uint64 totalCancelledAuctions; - /** @brief Auction creation and Batch bid service fees accumulated during the epoch, distributed at `END_EPOCH`. */ - uint64 pendingServiceFeePool; + /** @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; @@ -881,10 +897,22 @@ struct NOST : public ContractBase struct GetPendingServiceFeePool_output { - /** @brief Auction creation and Batch bid service fees accumulated during the epoch, distributed at `END_EPOCH`. */ + /** @brief Aggregate fee amount still awaiting `END_EPOCH` settlement. */ uint64 pendingServiceFeePool; }; + /** @brief Input payload used to inspect the detailed shared Nostromo fee pool. */ + using GetNostromoFeePool_input = NoData; + + struct GetNostromoFeePool_output + { + /** @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; + }; + /** @brief Input used to inspect a wallet's registered QU payout. */ struct GetPendingPayout_input { @@ -1748,17 +1776,25 @@ struct NOST : public ContractBase uint8 success; }; - /** @brief Internal input used to split private-auction and cancellation service fees between shareholders and configured recipients. */ - struct DistributeAuctionServiceFee_input + /** @brief Internal input used to accrue a service fee in the shared Nostromo fee pool. */ + struct AccumulateAuctionServiceFee_input { - /** @brief Fee amount that should be distributed. */ + /** @brief Fee amount that should be accumulated. */ uint64 feeAmount; }; - /** @brief Internal output returned after service-fee distribution is completed. */ - struct DistributeAuctionServiceFee_output + /** @brief Internal output returned after service-fee accrual is completed. */ + struct AccumulateAuctionServiceFee_output { - /** @brief Flag indicating whether the service-fee distribution completed successfully. */ + /** @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; }; @@ -1935,6 +1971,8 @@ struct NOST : public ContractBase ArchiveParticipant_output archiveParticipantOutput; QueueAndFlushQuPayout_input payoutInput; QueueAndFlushQuPayout_output payoutOutput; + AccumulateAuctionServiceFee_input accumulateAuctionServiceFeeInput; + AccumulateAuctionServiceFee_output accumulateAuctionServiceFeeOutput; uint64 activeQuantity; uint64 displacedQuantity; uint64 displacedRefund; @@ -2158,17 +2196,24 @@ struct NOST : public ContractBase struct DistributeAuctionRevenue_locals { AuctionRevenueBreakdown auctionRevenueBreakdown; + NostromoFeePool feePool; QueueAndFlushQuPayout_input payoutInput; QueueAndFlushQuPayout_output payoutOutput; - uint64 distributedDividendAmount; - uint64 dividendPerShare; + uint64 shareholderFeeTierIndex; }; - struct DistributeAuctionServiceFee_locals + struct AccumulateAuctionServiceFee_locals + { + NostromoFeePool feePool; + }; + + struct DistributeNostromoFeePool_locals { AuctionServiceFeeBreakdown auctionServiceFeeBreakdown; + NostromoFeePool feePool; QueueAndFlushQuPayout_input payoutInput; QueueAndFlushQuPayout_output payoutOutput; + uint64 shareholderDividendAmount; uint64 distributedDividendAmount; uint64 dividendPerShare; }; @@ -2207,7 +2252,7 @@ struct NOST : public ContractBase VerifyAuctionLotBalances_input verifyAuctionLotBalancesInput; EscrowAuctionLotAssets_input escrowAuctionLotAssetsInput; RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; - DistributeAuctionServiceFee_input distributeAuctionServiceFeeInput; + AccumulateAuctionServiceFee_input accumulateAuctionServiceFeeInput; sint64 requiredFee; sint64 existingRequiredAccessQuantity; uint64 resolvedQuantityForSale; @@ -2217,7 +2262,7 @@ struct NOST : public ContractBase RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; EscrowAuctionLotAssets_output escrowAuctionLotAssetsOutput; VerifyAuctionLotBalances_output verifyAuctionLotBalancesOutput; - DistributeAuctionServiceFee_output distributeAuctionServiceFeeOutput; + AccumulateAuctionServiceFee_output accumulateAuctionServiceFeeOutput; }; struct PlaceBid_locals @@ -2248,8 +2293,8 @@ struct NOST : public ContractBase NostromoProcedureLog log; RollbackAuctionLotAssets_input rollbackAuctionLotAssetsInput; RollbackAuctionLotAssets_output rollbackAuctionLotAssetsOutput; - DistributeAuctionServiceFee_input distributeAuctionServiceFeeInput; - DistributeAuctionServiceFee_output distributeAuctionServiceFeeOutput; + AccumulateAuctionServiceFee_input accumulateAuctionServiceFeeInput; + AccumulateAuctionServiceFee_output accumulateAuctionServiceFeeOutput; ArchiveClosedAuction_input archiveClosedAuctionInput; ArchiveClosedAuction_output archiveClosedAuctionOutput; DateAndTime currentDate; @@ -2300,8 +2345,8 @@ struct NOST : public ContractBase struct END_EPOCH_locals { - DistributeAuctionServiceFee_input distributeAuctionServiceFeeInput; - DistributeAuctionServiceFee_output distributeAuctionServiceFeeOutput; + DistributeNostromoFeePool_input distributeNostromoFeePoolInput; + DistributeNostromoFeePool_output distributeNostromoFeePoolOutput; ProcessPendingQuPayouts_input processPendingQuPayoutsInput; ProcessPendingQuPayouts_output processPendingQuPayoutsOutput; }; @@ -2400,6 +2445,7 @@ struct NOST : public ContractBase 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)); } /** @@ -2504,23 +2550,13 @@ struct NOST : public ContractBase } /** - * @brief Retries pending QU payouts, distributes pending service fees, and performs storage cleanup. + * @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); - // Service fees collected during the epoch are distributed as one batch to avoid repeated dividend dust handling. - if (state.get().pendingServiceFeePool > 0) - { - locals.distributeAuctionServiceFeeInput.feeAmount = state.get().pendingServiceFeePool; - CALL(DistributeAuctionServiceFee, locals.distributeAuctionServiceFeeInput, locals.distributeAuctionServiceFeeOutput); - // Clear the pool only after all liabilities were registered, so a capacity failure can be retried next epoch. - if (locals.distributeAuctionServiceFeeOutput.success) - { - state.mut().pendingServiceFeePool = 0; - } - } + CALL(DistributeNostromoFeePool, locals.distributeNostromoFeePoolInput, locals.distributeNostromoFeePoolOutput); state.mut().auctionList.cleanupIfNeeded(); state.mut().pendingQuPayouts.cleanupIfNeeded(); @@ -3241,7 +3277,7 @@ struct NOST : public ContractBase } /** - * @brief Splits auction sale revenue between the seller and configured fee recipients. + * @brief Pays auction sale proceeds to the seller and records every fee for end-of-epoch settlement. */ PRIVATE_PROCEDURE_WITH_LOCALS(DistributeAuctionRevenue) { @@ -3254,7 +3290,7 @@ struct NOST : public ContractBase output.success = 1; return; } - // Reserve worst-case recipient headroom before any fee liability is queued, keeping failure atomic. + // 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; @@ -3263,153 +3299,161 @@ struct NOST : public ContractBase calculateAuctionRevenueBreakdown(input.grossAmount, state, locals.auctionRevenueBreakdown); output.sellerPayout = locals.auctionRevenueBreakdown.sellerPayout; - // The temporary routing switch keeps seller payout math unchanged while sending every fee to development. + // 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)) { - if (input.grossAmount > output.sellerPayout) - { - locals.payoutInput.recipient = state.get().development; - locals.payoutInput.amount = input.grossAmount - 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.developmentAmount = sadd(locals.feePool.developmentAmount, input.grossAmount - output.sellerPayout); } - // Normal routing preserves each configured recipient and accumulates the shareholder portion separately. else { - if (locals.auctionRevenueBreakdown.managementFeeAmount > 0) - { - locals.payoutInput.recipient = state.get().management; - locals.payoutInput.amount = locals.auctionRevenueBreakdown.managementFeeAmount; - locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; - CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); - if (!locals.payoutOutput.success) - { - return; - } - } - if (locals.auctionRevenueBreakdown.developmentFeeAmount > 0) + locals.shareholderFeeTierIndex = getAuctionShareholderFeeTierIndex(input.grossAmount); + switch (locals.shareholderFeeTierIndex) { - locals.payoutInput.recipient = state.get().development; - locals.payoutInput.amount = locals.auctionRevenueBreakdown.developmentFeeAmount; - locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; - CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); - if (!locals.payoutOutput.success) - { - return; - } - } - if (locals.auctionRevenueBreakdown.takeoverCoordinatorFeeAmount > 0) - { - locals.payoutInput.recipient = state.get().takeoverCoordinator; - locals.payoutInput.amount = locals.auctionRevenueBreakdown.takeoverCoordinatorFeeAmount; - locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; - CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); - if (!locals.payoutOutput.success) - { - return; - } + 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; } - // Dividend dust stays pooled until it can be distributed evenly to all computors. - state.mut().auctionShareholderDividendPool = - sadd(state.get().auctionShareholderDividendPool, locals.auctionRevenueBreakdown.shareholderDividendAmount); - 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; - } + 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); } - 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) + 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 Distributes accumulated service fees to shareholders and configured recipients. + * @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(DistributeAuctionServiceFee) + PRIVATE_PROCEDURE_WITH_LOCALS(DistributeNostromoFeePool) { output.success = 0; + locals.feePool = state.get().feePool; - // Creation and cancellation paths may call this with zero after fee configuration changes. - if (input.feeAmount == 0) + if (locals.feePool.commonServiceFeeAmount > 0) { - output.success = 1; - return; + 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; } - // Management, development, and coordinator may each require a new liability slot. - if (state.get().pendingQuPayouts.population() > state.get().pendingQuPayouts.capacity() - NOST_AUCTION_SERVICE_FEE_MAX_PAYOUT_RECIPIENTS) + + 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) { - return; + 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; } - // Service fees use fixed recipients unless the runtime override sends all fees to development. - if (routeAllFeesToDevelopment(state)) + locals.dividendPerShare = div(state.get().auctionShareholderDividendPool, NUMBER_OF_COMPUTORS); + if (locals.dividendPerShare > 0 && qpi.distributeDividends(locals.dividendPerShare)) { - locals.payoutInput.recipient = state.get().development; - locals.payoutInput.amount = input.feeAmount; - locals.payoutInput.maxChunks = NOST_MAX_QU_TRANSFER_CHUNKS_PER_CALL; - CALL(QueueAndFlushQuPayout, locals.payoutInput, locals.payoutOutput); - output.success = locals.payoutOutput.success; - return; + locals.distributedDividendAmount = smul(locals.dividendPerShare, static_cast(NUMBER_OF_COMPUTORS)); + state.mut().auctionShareholderDividendPool -= locals.distributedDividendAmount; } - calculateAuctionServiceFeeBreakdown(input.feeAmount, locals.auctionServiceFeeBreakdown); - if (locals.auctionServiceFeeBreakdown.managementFeeAmount > 0) + if (state.get().feePool.managementAmount > 0) { locals.payoutInput.recipient = state.get().management; - locals.payoutInput.amount = locals.auctionServiceFeeBreakdown.managementFeeAmount; + 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 (locals.auctionServiceFeeBreakdown.developmentFeeAmount > 0) + if (state.get().feePool.developmentAmount > 0) { locals.payoutInput.recipient = state.get().development; - locals.payoutInput.amount = locals.auctionServiceFeeBreakdown.developmentFeeAmount; + 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 (locals.auctionServiceFeeBreakdown.takeoverCoordinatorFeeAmount > 0) + if (state.get().feePool.takeoverCoordinatorAmount > 0) { locals.payoutInput.recipient = state.get().takeoverCoordinator; - locals.payoutInput.amount = locals.auctionServiceFeeBreakdown.takeoverCoordinatorFeeAmount; + 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().auctionShareholderDividendPool = - sadd(state.get().auctionShareholderDividendPool, locals.auctionServiceFeeBreakdown.shareholderDividendAmount); - 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; + state.mut().feePool.takeoverCoordinatorAmount = 0; } output.success = 1; @@ -3889,7 +3933,8 @@ struct NOST : public ContractBase // Small-bid service fees are retained even if the bid is later displaced. if (locals.bidFeeCalculation.fee > 0) { - state.mut().pendingServiceFeePool = sadd(state.get().pendingServiceFeePool, locals.bidFeeCalculation.fee); + locals.accumulateAuctionServiceFeeInput.feeAmount = locals.bidFeeCalculation.fee; + CALL(AccumulateAuctionServiceFee, locals.accumulateAuctionServiceFeeInput, locals.accumulateAuctionServiceFeeOutput); } if (static_cast(qpi.invocationReward()) > locals.bidFeeCalculation.requiredReward) @@ -4823,7 +4868,8 @@ struct NOST : public ContractBase // Creation fees are held until END_EPOCH; overpayment is returned immediately. if (locals.requiredFee > 0) { - state.mut().pendingServiceFeePool = sadd(state.get().pendingServiceFeePool, static_cast(locals.requiredFee)); + locals.accumulateAuctionServiceFeeInput.feeAmount = static_cast(locals.requiredFee); + CALL(AccumulateAuctionServiceFee, locals.accumulateAuctionServiceFeeInput, locals.accumulateAuctionServiceFeeOutput); } if (qpi.invocationReward() > locals.requiredFee) @@ -5087,19 +5133,6 @@ struct NOST : public ContractBase return; } - if (state.get().pendingQuPayouts.population() > state.get().pendingQuPayouts.capacity() - NOST_AUCTION_SERVICE_FEE_MAX_PAYOUT_RECIPIENTS) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - output.errorCode = EAuctionError::PayoutQueueFull; - setProcedureLogInput(locals.log, qpi.invocator(), EProcedureId::CancelAuction, output.errorCode, input.auctionIndex, - output.cancellationFee); - logProcedureResult(locals.log); - return; - } - // The fee base represents the full reserve value of the lot being withdrawn. locals.cancellationBaseAmount = locals.auction.core.salePrice; if (locals.auction.core.type == EAuctionType::Batch) @@ -5139,9 +5172,9 @@ struct NOST : public ContractBase locals.archiveClosedAuctionInput.auction = locals.auction; CALL(ArchiveClosedAuction, locals.archiveClosedAuctionInput, locals.archiveClosedAuctionOutput); - // Cancellation fees are distributed immediately because cancellation is already a settlement action. - locals.distributeAuctionServiceFeeInput.feeAmount = output.cancellationFee; - CALL(DistributeAuctionServiceFee, locals.distributeAuctionServiceFeeInput, locals.distributeAuctionServiceFeeOutput); + // 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) { @@ -5628,9 +5661,17 @@ struct NOST : public ContractBase PUBLIC_FUNCTION(GetRouteAllFeesToDevelopment) { output.enabled = state.get().routeAllFeesToDevelopment; } /** - * @brief Returns the amount of accumulated auction service fees awaiting distribution at `END_EPOCH`. + * @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 = state.get().pendingServiceFeePool; } + 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); + } /** @brief Returns the QU obligation currently registered for one wallet. */ PUBLIC_FUNCTION(GetPendingPayout) @@ -5661,7 +5702,7 @@ struct NOST : public ContractBase output.stats.totalAuctionsCreated = state.get().totalAuctionsCreated; output.stats.closedAuctionHistoryCounter = state.get().closedAuctionHistoryCounter; output.stats.auctionShareholderDividendPool = state.get().auctionShareholderDividendPool; - output.stats.pendingServiceFeePool = state.get().pendingServiceFeePool; + 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()); @@ -6277,6 +6318,33 @@ struct NOST : public ContractBase return getAuctionShareholderFeeBasisPoints(grossAmount, state.get()); } + /** @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) + { + return 0; + } + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_2) + { + return 1; + } + if (grossAmount <= NOST_AUCTION_SHAREHOLDER_FEE_THRESHOLD_TIER_3) + { + return 2; + } + return 3; + } + + /** @brief Returns the saturating aggregate of every unsettled fee-pool accumulator. */ + static uint64 getNostromoFeePoolTotal(const NostromoFeePool& feePool) + { + 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))); + } + /** * @brief Computes `floor(amount * basisPoints / 10000)` without overflowing the intermediate product. */ @@ -6307,7 +6375,7 @@ struct NOST : public ContractBase /** * @brief Computes the exact service-fee split without performing transfers. - * @note Keep this helper pure so tests can reuse the same arithmetic as `DistributeAuctionServiceFee`. + * @note Keep this helper pure so tests can reuse the same arithmetic as `DistributeNostromoFeePool`. */ static void calculateAuctionServiceFeeBreakdown(uint64 feeAmount, AuctionServiceFeeBreakdown& output) { diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index c3824d77f..1d691d5e6 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -558,6 +558,15 @@ class ContractTestingNOST : protected ContractTesting 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{}; @@ -1361,6 +1370,7 @@ TEST(ContractNostromoAuction, AcceptedBatchBidAccumulatesFeeAndKeepsEscrowAuctio 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. @@ -1805,9 +1815,16 @@ TEST(ContractNostromoAuction, PrivateAuctionFeeIsDistributedAcrossRecipientsAuct 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) { @@ -1827,6 +1844,35 @@ TEST(ContractNostromoAuction, PrivateAuctionFeeIsDistributedAcrossRecipientsAuct } } +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); + + NOST::AuctionServiceFeeBreakdown expectedBreakdown{}; + nostromo.calculateAuctionServiceFeeBreakdown(static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE), expectedBreakdown); + 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, expectedBreakdown.managementFeeAmount); +} + TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) { ContractTestingNOST nostromo; @@ -2992,11 +3038,13 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) NOST::AuctionRevenueBreakdown expectedRevenue{}; nostromo.calculateAuctionRevenueBreakdown(NOST_STANDARD_MIN_PRICE, expectedRevenue); - const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedRevenue.shareholderDividendAmount); 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(); + const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedRevenue.shareholderDividendAmount); const sint64 sellerBalanceBefore = getBalance(seller); const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); @@ -3012,6 +3060,15 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) EXPECT_EQ(auction.core.allocatedQuantity, 1ULL); EXPECT_EQ(nostromo.managedShares(asset, bidder), 1); EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, expectedRevenue.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, + static_cast(NOST_STANDARD_MIN_PRICE - expectedRevenue.sellerPayout)); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, NOST_STANDARD_MIN_PRICE - expectedRevenue.sellerPayout); + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 0ULL); if (routeMode != 0) { @@ -3152,6 +3209,7 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) 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()); @@ -3168,6 +3226,12 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) 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); @@ -3199,6 +3263,7 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) 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()); @@ -3215,6 +3280,12 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) 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); @@ -3252,6 +3323,7 @@ TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeR const auto batchCreateOutput = nostromo.createAuction(batchSeller, ContractTestingNOST::makeBatchAuctionInput(batchAsset, 7, 333)); ASSERT_EQ(batchCreateOutput.errorCode, NOST::EAuctionError::Success); + nostromo.endEpoch(); NOST::AuctionServiceFeeBreakdown expectedBatchBreakdown{}; nostromo.calculateAuctionServiceFeeBreakdown(233ULL, expectedBatchBreakdown); @@ -3264,6 +3336,12 @@ TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeR 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); @@ -3298,6 +3376,7 @@ TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeR const auto standardCreateOutput = smallFeeNostromo.createAuction(standardSeller, ContractTestingNOST::makeBatchAuctionInput(standardAsset, 1, 19)); ASSERT_EQ(standardCreateOutput.errorCode, NOST::EAuctionError::Success); + smallFeeNostromo.endEpoch(); NOST::AuctionServiceFeeBreakdown expectedSmallBreakdown{}; smallFeeNostromo.calculateAuctionServiceFeeBreakdown(1ULL, expectedSmallBreakdown); @@ -3313,6 +3392,10 @@ TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeR EXPECT_EQ(expectedSmallBreakdown.managementFeeAmount, 0ULL); EXPECT_EQ(expectedSmallBreakdown.developmentFeeAmount, 0ULL); EXPECT_EQ(expectedSmallBreakdown.takeoverCoordinatorFeeAmount, 0ULL); + 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); @@ -3687,7 +3770,6 @@ TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) const Asset asset{seller, cases[caseIndex].assetName}; NOST::AuctionRevenueBreakdown expectedRevenue{}; nostromo.calculateAuctionRevenueBreakdown(cases[caseIndex].grossAmount, expectedRevenue); - const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedRevenue.shareholderDividendAmount); nostromo.setRouteAllFeesToDevelopment(routeMode); EXPECT_EQ(nostromo.issueAsset(seller, cases[caseIndex].assetName, 1), 1); @@ -3697,6 +3779,8 @@ TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) 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(expectedRevenue.shareholderDividendAmount); const sint64 sellerBefore = getBalance(seller); const sint64 managementBefore = getBalance(ContractTestingNOST::managementWallet()); const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); @@ -3709,6 +3793,28 @@ TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) EXPECT_EQ(nostromo.getAuctionShareholderFeeBasisPoints(cases[caseIndex].grossAmount), cases[caseIndex].expectedShareholderFeeBp); EXPECT_EQ(getBalance(seller) - sellerBefore, expectedRevenue.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, + static_cast(cases[caseIndex].grossAmount - expectedRevenue.sellerPayout)); + + const auto pendingFeePool = nostromo.getNostromoFeePool(); + EXPECT_EQ(pendingFeePool.totalAmount, cases[caseIndex].grossAmount - expectedRevenue.sellerPayout); + 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 ? expectedRevenue.shareholderDividendAmount : 0ULL); + } + } + + nostromo.endEpoch(); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 0ULL); if (routeMode != 0) { From ddb0abd4c1e866d8183eb4b644d05046fd0a7929 Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 13 Aug 2026 18:37:29 +0300 Subject: [PATCH 57/59] remove NOSTChecker class and streamline auction fee calculations --- test/contract_nostromo.cpp | 427 ++++++++++++++++++++----------------- 1 file changed, 236 insertions(+), 191 deletions(-) diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index 1d691d5e6..a16d2233b 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -11,28 +11,6 @@ namespace static const id NOST_CONTRACT_ID(NOST_CONTRACT_INDEX, 0, 0, 0); } // namespace -class NOSTChecker : public NOST, public NOST::StateData -{ - -public: - const QPI::ContractState& asState() const - { - return *reinterpret_cast*>(static_cast(this)); - } - - void calculateAuctionRevenueBreakdown(uint64 grossAmount, AuctionRevenueBreakdown& output) const - { - NOST::calculateAuctionRevenueBreakdown(grossAmount, asState(), output); - } - - void calculateAuctionServiceFeeBreakdown(uint64 feeAmount, AuctionServiceFeeBreakdown& output) const - { - NOST::calculateAuctionServiceFeeBreakdown(feeAmount, output); - } - - uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount) const { return NOST::getAuctionShareholderFeeBasisPoints(grossAmount, asState()); } -}; - class ContractTestingNOST : protected ContractTesting { public: @@ -50,8 +28,6 @@ class ContractTestingNOST : protected ContractTesting callSystemProcedure(NOST_CONTRACT_INDEX, END_TICK); } - NOSTChecker* state() { return reinterpret_cast(contractStates[NOST_CONTRACT_INDEX]); } - void ensureUser(const id& user, sint64 amount = 1000) { if (getBalance(user) == 0) @@ -190,13 +166,13 @@ class ContractTestingNOST : protected ContractTesting NOST::PlaceBid_output placeBatchBidWithRequiredReward(const id& bidder, uint64 auctionIndex, uint64 bidQuantity, uint64 bidAmount) { - const auto calculation = calculateBatchAuctionBidFee(bidQuantity, 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 auto calculation = calculateBatchAuctionBidFee(bidQuantity, bidAmount); + const NOST::CalculateBatchAuctionBidFee_output& calculation = calculateBatchAuctionBidFee(bidQuantity, bidAmount); return placeBidWithFundedReward(bidder, auctionIndex, bidQuantity, bidAmount, static_cast(calculation.requiredReward)); } @@ -333,7 +309,7 @@ class ContractTestingNOST : protected ContractTesting NOST::SetAuctionFees_input makeCoordinatorFeeInput(sint64 publicAuctionCreationFee) const { - const auto fees = getAuctionFees(); + const NOST::GetAuctionFees_output& fees = getAuctionFees(); NOST::SetAuctionFees_input input{}; input.privateAuctionFee = fees.privateAuctionFee; input.publicAuctionCreationFee = publicAuctionCreationFee; @@ -351,7 +327,7 @@ class ContractTestingNOST : protected ContractTesting NOST::SetAuctionFeesByManagement_input makeManagementFeeInput(sint64 publicAuctionCreationFee) const { - const auto fees = getAuctionFees(); + const NOST::GetAuctionFees_output& fees = getAuctionFees(); NOST::SetAuctionFeesByManagement_input input{}; input.privateAuctionFee = fees.privateAuctionFee; input.publicAuctionCreationFee = publicAuctionCreationFee; @@ -716,16 +692,6 @@ class ContractTestingNOST : protected ContractTesting return input; } - void calculateAuctionRevenueBreakdown(uint64 grossAmount, NOST::AuctionRevenueBreakdown& output) - { - state()->calculateAuctionRevenueBreakdown(grossAmount, output); - } - - void calculateAuctionServiceFeeBreakdown(uint64 feeAmount, NOST::AuctionServiceFeeBreakdown& output) - { - state()->calculateAuctionServiceFeeBreakdown(feeAmount, output); - } - sint64 expectedDividendPoolIncrease(uint64 addedDividendAmount) const { const uint64 poolBefore = stateData().auctionShareholderDividendPool; @@ -733,8 +699,6 @@ class ContractTestingNOST : protected ContractTesting return static_cast(poolAfterFunding % NUMBER_OF_COMPUTORS) - static_cast(poolBefore); } - uint64 getAuctionShareholderFeeBasisPoints(uint64 grossAmount) { return state()->getAuctionShareholderFeeBasisPoints(grossAmount); } - 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, @@ -792,6 +756,21 @@ static bool containsAuctionIndex(const Array& return false; } +static void expectAuctionFeesEqual(const NOST::GetAuctionFees_output& actual, const NOST::GetAuctionFees_output& expected) +{ + EXPECT_EQ(actual.privateAuctionFee, expected.privateAuctionFee); + EXPECT_EQ(actual.publicAuctionCreationFee, expected.publicAuctionCreationFee); + EXPECT_EQ(actual.auctionCancellationFeeBasisPoints, expected.auctionCancellationFeeBasisPoints); + EXPECT_EQ(actual.managementFeeBasisPoints, expected.managementFeeBasisPoints); + EXPECT_EQ(actual.developmentFeeBasisPoints, expected.developmentFeeBasisPoints); + EXPECT_EQ(actual.takeoverCoordinatorFeeBasisPoints, expected.takeoverCoordinatorFeeBasisPoints); + EXPECT_EQ(actual.shareholderDividendBasisPoints, expected.shareholderDividendBasisPoints); + EXPECT_EQ(actual.shareholderFeeBasisPointsTier1, expected.shareholderFeeBasisPointsTier1); + EXPECT_EQ(actual.shareholderFeeBasisPointsTier2, expected.shareholderFeeBasisPointsTier2); + EXPECT_EQ(actual.shareholderFeeBasisPointsTier3, expected.shareholderFeeBasisPointsTier3); + EXPECT_EQ(actual.shareholderFeeBasisPointsTier4, expected.shareholderFeeBasisPointsTier4); +} + TEST(ContractNostromoAuction, InitialStateAndGettersAuction) { ContractTestingNOST nostromo; @@ -944,8 +923,9 @@ TEST(ContractNostromoAuction, RetainedAuctionGetterPaginationAcrossLiveAndClosed 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) { + 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; @@ -970,8 +950,7 @@ TEST(ContractNostromoAuction, RetainedAuctionGetterPaginationAcrossLiveAndClosed 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); + 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)); @@ -1218,8 +1197,7 @@ TEST(ContractNostromoAuction, PublicAuctionCreationAccumulatesConfiguredFeeAndRe const sint64 standardSellerBefore = getBalance(standardSeller); const sint64 standardContractBefore = getBalance(NOST_CONTRACT_ID); - const auto standardInsufficient = - nostromo.createAuctionWithFundedReward(standardSeller, standardInput, managementConfiguredFee - 1); + 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); @@ -1260,8 +1238,7 @@ TEST(ContractNostromoAuction, PublicAuctionCreationAccumulatesConfiguredFeeAndRe 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)); + 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); @@ -1293,6 +1270,7 @@ TEST(ContractNostromoAuction, BatchBidFeeBoundariesAuction) 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); @@ -1401,8 +1379,7 @@ TEST(ContractNostromoAuction, CreateStandardSingleAssetAuctionEscrowsLotAuction) 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)); + 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); @@ -1778,6 +1755,7 @@ TEST(ContractNostromoAuction, PrivateAuctionFeeIsDistributedAcrossRecipientsAuct 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); @@ -1796,9 +1774,11 @@ TEST(ContractNostromoAuction, PrivateAuctionFeeIsDistributedAcrossRecipientsAuct const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - NOST::AuctionServiceFeeBreakdown expectedBreakdown{}; - nostromo.calculateAuctionServiceFeeBreakdown(static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE), expectedBreakdown); - const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedBreakdown.shareholderDividendAmount); + 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); @@ -1817,10 +1797,8 @@ TEST(ContractNostromoAuction, PrivateAuctionFeeIsDistributedAcrossRecipientsAuct 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); + 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); @@ -1835,10 +1813,9 @@ TEST(ContractNostromoAuction, PrivateAuctionFeeIsDistributedAcrossRecipientsAuct } else { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedBreakdown.managementFeeAmount); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedBreakdown.developmentFeeAmount); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, - expectedBreakdown.takeoverCoordinatorFeeAmount); + 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); } } @@ -1860,17 +1837,14 @@ TEST(ContractNostromoAuction, EndEpochUsesCurrentManagementWalletForAccruedFeesA input.allowedBidderWallets = ContractTestingNOST::makeAllowedWallets({allowedBidder}); ASSERT_EQ(nostromo.createAuction(seller, input, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, NOST::EAuctionError::Success); - NOST::AuctionServiceFeeBreakdown expectedBreakdown{}; - nostromo.calculateAuctionServiceFeeBreakdown(static_cast(NOST_DEFAULT_PRIVATE_AUCTION_FEE), expectedBreakdown); 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); + ASSERT_EQ(nostromo.setManagement(ContractTestingNOST::takeoverCoordinatorWallet(), newManagement).errorCode, NOST::EAuctionError::Success); nostromo.endEpoch(); EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()), previousManagementBefore); - EXPECT_EQ(getBalance(newManagement) - newManagementBefore, expectedBreakdown.managementFeeAmount); + EXPECT_EQ(getBalance(newManagement) - newManagementBefore, 4550000ULL); } TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) @@ -1885,95 +1859,110 @@ TEST(ContractNostromoAuction, CreateAuctionRejectsInvalidInputsAuction) 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(nostromo.createAuction(seller, invalidCid).errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(invokeRejectedPublicAuction(invalidCid).errorCode, NOST::EAuctionError::InvalidInput); auto invalidCidUppercase = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); invalidCidUppercase.metadataIpfsCid = ContractTestingNOST::makeInvalidMetadataCidUppercase(); - EXPECT_EQ(nostromo.createAuction(seller, invalidCidUppercase).errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(invokeRejectedPublicAuction(invalidCidUppercase).errorCode, NOST::EAuctionError::InvalidInput); auto emptyLot = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); emptyLot.auctionLotItems = Array{}; - EXPECT_EQ(nostromo.createAuction(seller, emptyLot).errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(invokeRejectedPublicAuction(emptyLot).errorCode, NOST::EAuctionError::InvalidInput); auto negativeQuantity = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); negativeQuantity.auctionLotItems = ContractTestingNOST::makeSingleLot(assetA, -1); - EXPECT_EQ(nostromo.createAuction(seller, negativeQuantity).errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(invokeRejectedPublicAuction(negativeQuantity).errorCode, NOST::EAuctionError::InvalidInput); auto zeroDuration = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); zeroDuration.durationDays = 0; - EXPECT_EQ(nostromo.createAuction(seller, zeroDuration).errorCode, NOST::EAuctionError::InvalidInput); + 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(nostromo.createAuction(seller, tooLongDuration).errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(invokeRejectedPublicAuction(tooLongDuration).errorCode, NOST::EAuctionError::InvalidInput); auto invalidType = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); invalidType.auctionType = 99; - EXPECT_EQ(nostromo.createAuction(seller, invalidType).errorCode, NOST::EAuctionError::InvalidAuctionType); + EXPECT_EQ(invokeRejectedPublicAuction(invalidType).errorCode, NOST::EAuctionError::InvalidAuctionType); auto invalidVisibility = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); invalidVisibility.auctionVisibility = 99; - EXPECT_EQ(nostromo.createAuction(seller, invalidVisibility).errorCode, NOST::EAuctionError::InvalidVisibility); + EXPECT_EQ(invokeRejectedPublicAuction(invalidVisibility).errorCode, NOST::EAuctionError::InvalidVisibility); auto partiallyEmptyLot = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); partiallyEmptyLot.auctionLotItems = ContractTestingNOST::makeSingleLot(Asset{}, 1); - EXPECT_EQ(nostromo.createAuction(seller, partiallyEmptyLot).errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(invokeRejectedPublicAuction(partiallyEmptyLot).errorCode, NOST::EAuctionError::InvalidInput); auto invalidBatchBuyNow = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); invalidBatchBuyNow.buyNowPrice = 100; - EXPECT_EQ(nostromo.createAuction(seller, invalidBatchBuyNow).errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(invokeRejectedPublicAuction(invalidBatchBuyNow).errorCode, NOST::EAuctionError::InvalidInput); auto invalidStandardIncrement = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); invalidStandardIncrement.minimumBidIncrement = 0; - EXPECT_EQ(nostromo.createAuction(seller, invalidStandardIncrement).errorCode, NOST::EAuctionError::InvalidInput); + 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(nostromo.createAuction(seller, invalidStandardLowInitial).errorCode, NOST::EAuctionError::InvalidInput); + 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(nostromo.createAuction(seller, invalidStandardLowSale).errorCode, NOST::EAuctionError::InvalidInput); + 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(nostromo.createAuction(seller, invalidStandardLowIncrement).errorCode, NOST::EAuctionError::InvalidInput); + 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(nostromo.createAuction(seller, invalidStandardPrice).errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardPrice).errorCode, NOST::EAuctionError::InvalidInput); auto invalidStandardSalePrice = ContractTestingNOST::makeStandardAuctionInput(ContractTestingNOST::makeSingleLot(assetA, 1)); invalidStandardSalePrice.salePrice = 0; - EXPECT_EQ(nostromo.createAuction(seller, invalidStandardSalePrice).errorCode, NOST::EAuctionError::InvalidInput); + 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(nostromo.createAuction(seller, invalidStandardBuyNow).errorCode, NOST::EAuctionError::InvalidInput); + EXPECT_EQ(invokeRejectedPublicAuction(invalidStandardBuyNow).errorCode, NOST::EAuctionError::InvalidInput); auto privateWithoutGate = ContractTestingNOST::makeBatchAuctionInput(assetA, 5, 10); privateWithoutGate.auctionVisibility = static_cast(NOST::EAuctionVisibility::Private); - EXPECT_EQ(nostromo.createAuction(seller, privateWithoutGate, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, NOST::EAuctionError::InvalidInput); + 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(nostromo.createAuction(seller, zeroAccessQuantity, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, NOST::EAuctionError::InvalidInput); + 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(nostromo.createAuction(seller, negativeAccessQuantity, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, NOST::EAuctionError::InvalidInput); + 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(nostromo.createAuction(seller, partiallyEmptyAccessAsset, NOST_DEFAULT_PRIVATE_AUCTION_FEE).errorCode, - NOST::EAuctionError::InvalidInput); + 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) @@ -2386,6 +2375,7 @@ TEST(ContractNostromoAuction, DeterministicBatchAllocationPropertiesAuction) // 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; @@ -2394,8 +2384,7 @@ TEST(ContractNostromoAuction, DeterministicBatchAllocationPropertiesAuction) 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.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); @@ -2405,16 +2394,19 @@ TEST(ContractNostromoAuction, DeterministicBatchAllocationPropertiesAuction) 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); - nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, bidQuantity, bidPrice); + ASSERT_EQ(nostromo.placeBatchBidWithRequiredReward(bidder, createOutput.auctionIndex, bidQuantity, bidPrice).errorCode, + NOST::EAuctionError::Success); } nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); const auto auction = nostromo.getAuction(createOutput.auctionIndex).auction; const auto participants = nostromo.getAuctionParticipants(createOutput.auctionIndex, 0, NOST_AUCTION_GETTER_PAGE_SIZE); + EXPECT_EQ(auction.core.status, NOST::EAuctionStatus::Finalized); uint64 allocatedTotal = 0; for (uint64 participantIndex = 0; participantIndex < participants.returnedCount; ++participantIndex) { @@ -2646,12 +2638,9 @@ TEST(ContractNostromoAuction, PrivateAuctionCombinedAccessUsesInclusiveOrAuction 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); + 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) @@ -2668,8 +2657,6 @@ TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) 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); - NOST::AuctionRevenueBreakdown expectedRevenue{}; - nostromo.calculateAuctionRevenueBreakdown(NOST_STANDARD_MIN_PRICE + 800000ULL, expectedRevenue); const auto createOutput = nostromo.createAuction(seller, input); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); const sint64 sellerBalanceBefore = getBalance(seller); @@ -2687,7 +2674,7 @@ TEST(ContractNostromoAuction, PlaceStandardBidBuyNowFinalizesImmediatelyAuction) EXPECT_EQ(participant.participantData.escrowedAmount, 0ULL); EXPECT_EQ(participant.participantData.isWinningBid, 1u); EXPECT_EQ(nostromo.managedShares(asset, bidder), 3); - EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, expectedRevenue.sellerPayout); + EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, 1683000ULL); } TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialFillAuction) @@ -2776,8 +2763,6 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF input.minimumPurchaseQuantity = 10; const auto createOutput = nostromo.createAuction(seller, input); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); - NOST::AuctionRevenueBreakdown expectedRevenue{}; - nostromo.calculateAuctionRevenueBreakdown(200ULL, expectedRevenue); const sint64 sellerBalanceBefore = getBalance(seller); nostromo.seedUser(firstBidder, 291); nostromo.seedUser(secondBidder, 150); @@ -2799,7 +2784,7 @@ TEST(ContractNostromoAuction, EndTickFinalizesBatchAuctionByPriceTimeAndPartialF 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, expectedRevenue.sellerPayout); + EXPECT_EQ(getBalance(seller) - sellerBalanceBefore, 187ULL); } { @@ -3025,6 +3010,7 @@ 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); @@ -3036,15 +3022,18 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) EXPECT_EQ(nostromo.issueAsset(seller, assetName, 1), 1); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); - NOST::AuctionRevenueBreakdown expectedRevenue{}; - nostromo.calculateAuctionRevenueBreakdown(NOST_STANDARD_MIN_PRICE, expectedRevenue); - 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(); - const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedRevenue.shareholderDividendAmount); + 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()); @@ -3059,13 +3048,12 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) 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, expectedRevenue.sellerPayout); + 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, - static_cast(NOST_STANDARD_MIN_PRICE - expectedRevenue.sellerPayout)); - EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, NOST_STANDARD_MIN_PRICE - expectedRevenue.sellerPayout); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, expectedTotalFees); + EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, expectedTotalFees); nostromo.endEpoch(); EXPECT_EQ(nostromo.getNostromoFeePool().totalAmount, 0ULL); @@ -3073,16 +3061,15 @@ TEST(ContractNostromoAuction, EndTickFinalizesStandardAuctionAtSalePriceAuction) if (routeMode != 0) { EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, - NOST_STANDARD_MIN_PRICE - expectedRevenue.sellerPayout); + EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedTotalFees); EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, 0); EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 0); } else { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedRevenue.managementFeeAmount); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedRevenue.developmentFeeAmount); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, expectedRevenue.takeoverCoordinatorFeeAmount); + 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); } } @@ -3196,6 +3183,7 @@ 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]; @@ -3215,9 +3203,9 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - NOST::AuctionServiceFeeBreakdown expectedBreakdown{}; - nostromo.calculateAuctionServiceFeeBreakdown(1000ULL, expectedBreakdown); - const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedBreakdown.shareholderDividendAmount); + 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); @@ -3241,10 +3229,9 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) } else { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedBreakdown.managementFeeAmount); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedBreakdown.developmentFeeAmount); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, - expectedBreakdown.takeoverCoordinatorFeeAmount); + 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); } } @@ -3269,9 +3256,9 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) const sint64 developmentBefore = getBalance(ContractTestingNOST::developmentWallet()); const sint64 coordinatorBefore = getBalance(ContractTestingNOST::takeoverCoordinatorWallet()); const sint64 contractBefore = getBalance(NOST_CONTRACT_ID); - NOST::AuctionServiceFeeBreakdown expectedBreakdown{}; - nostromo.calculateAuctionServiceFeeBreakdown(100000ULL, expectedBreakdown); - const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedBreakdown.shareholderDividendAmount); + 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); @@ -3295,10 +3282,9 @@ TEST(ContractNostromoAuction, CancelAuctionWithoutBidsDistributesFeeAuction) } else { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedBreakdown.managementFeeAmount); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedBreakdown.developmentFeeAmount); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, - expectedBreakdown.takeoverCoordinatorFeeAmount); + 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); } } @@ -3310,6 +3296,7 @@ TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeR 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]; @@ -3325,9 +3312,9 @@ TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeR ASSERT_EQ(batchCreateOutput.errorCode, NOST::EAuctionError::Success); nostromo.endEpoch(); - NOST::AuctionServiceFeeBreakdown expectedBatchBreakdown{}; - nostromo.calculateAuctionServiceFeeBreakdown(233ULL, expectedBatchBreakdown); - const sint64 expectedBatchDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedBatchBreakdown.shareholderDividendAmount); + 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()); @@ -3351,15 +3338,12 @@ TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeR } else { - EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, expectedBatchBreakdown.managementFeeAmount); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedBatchBreakdown.developmentFeeAmount); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, - expectedBatchBreakdown.takeoverCoordinatorFeeAmount); + 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(expectedBatchBreakdown.shareholderDividendAmount + expectedBatchBreakdown.managementFeeAmount + - expectedBatchBreakdown.developmentFeeAmount + expectedBatchBreakdown.takeoverCoordinatorFeeAmount, - batchCancelOutput.cancellationFee); + EXPECT_EQ(expectedBatchShareholderDividend + expectedBatchRecipientFee * 3ULL, batchCancelOutput.cancellationFee); } { @@ -3378,20 +3362,13 @@ TEST(ContractNostromoAuction, CancelAuctionUsesTruncatedFeeAndAssignsServiceFeeR ASSERT_EQ(standardCreateOutput.errorCode, NOST::EAuctionError::Success); smallFeeNostromo.endEpoch(); - NOST::AuctionServiceFeeBreakdown expectedSmallBreakdown{}; - smallFeeNostromo.calculateAuctionServiceFeeBreakdown(1ULL, expectedSmallBreakdown); - const sint64 expectedSmallDividendPoolIncrease = - smallFeeNostromo.expectedDividendPoolIncrease(expectedSmallBreakdown.shareholderDividendAmount); + 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(expectedSmallBreakdown.shareholderDividendAmount, 1ULL); - EXPECT_EQ(expectedSmallBreakdown.managementFeeAmount, 0ULL); - EXPECT_EQ(expectedSmallBreakdown.developmentFeeAmount, 0ULL); - EXPECT_EQ(expectedSmallBreakdown.takeoverCoordinatorFeeAmount, 0ULL); EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, 0); EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, 1ULL); EXPECT_EQ(smallFeeNostromo.getNostromoFeePool().totalAmount, 1ULL); @@ -3505,9 +3482,9 @@ TEST(ContractNostromoAuction, FinalizationArchivesRecordsAndReusesActiveSlotsAuc 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); + 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, @@ -3606,14 +3583,17 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) 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); @@ -3628,11 +3608,14 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) 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); @@ -3649,14 +3632,17 @@ TEST(ContractNostromoAuction, GovernanceAndFeeSettersAuction) 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); @@ -3681,12 +3667,19 @@ TEST(ContractNostromoAuction, BatchSettlementAutomaticallyFlushesLargeSellerPayo 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)); + 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) @@ -3697,22 +3690,17 @@ TEST(ContractNostromoAuction, BatchSettlementAutomaticallyFlushesLargeSellerPayo } const uint64 grossAmount = bidderCount * static_cast(MAX_AMOUNT); - NOST::AuctionRevenueBreakdown breakdown{}; - nostromo.calculateAuctionRevenueBreakdown(grossAmount, breakdown); - EXPECT_EQ(breakdown.sellerPayout + breakdown.shareholderDividendAmount + breakdown.managementFeeAmount + - breakdown.developmentFeeAmount + breakdown.takeoverCoordinatorFeeAmount, - grossAmount); 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(breakdown.sellerPayout, expectedImmediatePayout); + ASSERT_GT(grossAmount, expectedImmediatePayout); EXPECT_EQ(static_cast(getBalance(seller) - sellerBeforeSettlement), expectedImmediatePayout); - EXPECT_EQ(nostromo.getPendingPayout(seller).amount, breakdown.sellerPayout - expectedImmediatePayout); - EXPECT_EQ(nostromo.getContractStats().stats.totalPendingQuPayouts, breakdown.sellerPayout - 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), breakdown.sellerPayout); + EXPECT_EQ(static_cast(getBalance(seller) - sellerBeforeSettlement), grossAmount); EXPECT_EQ(nostromo.getPendingPayout(seller).amount, 0ULL); EXPECT_EQ(nostromo.getContractStats().stats.totalPendingQuPayouts, 0ULL); } @@ -3740,20 +3728,79 @@ TEST(ContractNostromoAuction, EndEpochPendingPayoutProcessingIsBoundedAuction) 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 expectedShareholderFeeBp; + uint64 sellerPayout; + uint64 shareholderDividend; + uint64 managementFee; + uint64 developmentFee; + uint64 coordinatorFee; + uint64 totalFee; uint64 assetName; }; const TierCase cases[] = { - {5000000000ULL, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_1, assetNameFromString("TIERA1")}, - {5000000001ULL, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_2, assetNameFromString("TIERA2")}, - {50000000001ULL, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_3, assetNameFromString("TIERA3")}, - {200000000001ULL, NOST_DEFAULT_AUCTION_SHAREHOLDER_FEE_BP_TIER_4, assetNameFromString("TIERA4")}, + {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}; @@ -3761,6 +3808,8 @@ TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) { 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, @@ -3768,9 +3817,6 @@ TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) 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}; - NOST::AuctionRevenueBreakdown expectedRevenue{}; - nostromo.calculateAuctionRevenueBreakdown(cases[caseIndex].grossAmount, expectedRevenue); - nostromo.setRouteAllFeesToDevelopment(routeMode); EXPECT_EQ(nostromo.issueAsset(seller, cases[caseIndex].assetName, 1), 1); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 1), 1); @@ -3780,7 +3826,7 @@ TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) cases[caseIndex].grossAmount, NOST_STANDARD_MIN_BID_INCREMENT)); ASSERT_EQ(createOutput.errorCode, NOST::EAuctionError::Success); nostromo.endEpoch(); - const sint64 expectedDividendPoolIncrease = nostromo.expectedDividendPoolIncrease(expectedRevenue.shareholderDividendAmount); + 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()); @@ -3791,25 +3837,22 @@ TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) nostromo.advanceAndEndTick((NOST_SECONDS_PER_DAY + 1ULL) * 1000ULL); - EXPECT_EQ(nostromo.getAuctionShareholderFeeBasisPoints(cases[caseIndex].grossAmount), cases[caseIndex].expectedShareholderFeeBp); - EXPECT_EQ(getBalance(seller) - sellerBefore, expectedRevenue.sellerPayout); + 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, - static_cast(cases[caseIndex].grossAmount - expectedRevenue.sellerPayout)); + EXPECT_EQ(getBalance(NOST_CONTRACT_ID) - contractBefore, cases[caseIndex].totalFee); const auto pendingFeePool = nostromo.getNostromoFeePool(); - EXPECT_EQ(pendingFeePool.totalAmount, cases[caseIndex].grossAmount - expectedRevenue.sellerPayout); + 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}; + 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 ? expectedRevenue.shareholderDividendAmount : 0ULL); + EXPECT_EQ(tierAmounts[tierIndex], tierIndex == caseIndex ? cases[caseIndex].shareholderDividend : 0ULL); } } @@ -3819,17 +3862,15 @@ TEST(ContractNostromoAuction, ShareholderFeeTiersAreAppliedAuction) if (routeMode != 0) { EXPECT_EQ(getBalance(ContractTestingNOST::managementWallet()) - managementBefore, 0); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, - cases[caseIndex].grossAmount - expectedRevenue.sellerPayout); + 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, expectedRevenue.managementFeeAmount); - EXPECT_EQ(getBalance(ContractTestingNOST::developmentWallet()) - developmentBefore, expectedRevenue.developmentFeeAmount); - EXPECT_EQ(getBalance(ContractTestingNOST::takeoverCoordinatorWallet()) - coordinatorBefore, - expectedRevenue.takeoverCoordinatorFeeAmount); + 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); } } @@ -3919,6 +3960,7 @@ TEST(ContractNostromoAuction, SetFeeReserveGuardConfigValidatesAndRestrictsCalle { 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); @@ -3928,10 +3970,13 @@ TEST(ContractNostromoAuction, SetFeeReserveGuardConfigValidatesAndRestrictsCalle 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); - auto guardState = nostromo.getFeeReserveGuardState(); + guardState = nostromo.getFeeReserveGuardState(); EXPECT_EQ(guardState.dropBasisPoints, 500ULL); EXPECT_EQ(guardState.windowSeconds, 300ULL); @@ -3949,7 +3994,7 @@ TEST(ContractNostromoAuction, EndEpochDistributesPendingFeesWhileEmergencyPaused EXPECT_EQ(nostromo.issueAsset(seller, asset.assetName, 3), 3); EXPECT_EQ(nostromo.transferShareManagementRightsToNostromo(seller, asset, 3), 3); - const auto createOutput = nostromo.createAuction(seller, ContractTestingNOST::makeBatchAuctionInput(asset, 3, 1)); + 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; From 02adbe20e2e92e2974a3988a3ac5f750de461e3f Mon Sep 17 00:00:00 2001 From: N-010 Date: Thu, 13 Aug 2026 20:42:15 +0300 Subject: [PATCH 58/59] Moved comments for verification processing --- src/contracts/Nostromo.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 2f03c3023..e3e34c840 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -2580,9 +2580,9 @@ struct NOST : public ContractBase state.mut().feeReserveBaseline = locals.currentReserve; state.mut().feeReserveBaselineAt = locals.currentDate; } - // Subsequent observations either trigger the guard or roll the baseline into a new window. 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) @@ -2645,9 +2645,9 @@ struct NOST : public ContractBase locals.finalizeStandardAuctionInput.currentDate = locals.currentDate; CALL(FinalizeStandardAuction, locals.finalizeStandardAuctionInput, locals.finalizeStandardAuctionOutput); } - // A funded bid below the seller's sale price requires an explicit, time-bounded seller choice. 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; @@ -2960,9 +2960,9 @@ struct NOST : public ContractBase return; } } - // First-time recipients consume a new map slot; failure leaves the global liability total unchanged. 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) { @@ -3655,9 +3655,9 @@ struct NOST : public ContractBase { output.availableQuantity = 0; } - // Otherwise expose the full unreserved quantity; the minimum check below decides whether bidding remains viable. else { + // Otherwise expose the full unreserved quantity; the minimum check below decides whether bidding remains viable. output.availableQuantity = locals.auction.core.quantityForSale - locals.salePriorityQuantity; } @@ -3667,9 +3667,9 @@ struct NOST : public ContractBase output.minimumBidPrice = locals.auction.core.salePrice; output.isAcceptingBids = 1; } - // A full book can still accept a strictly better bid that displaces the current lowest-priced allocation. 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) { From 046f22ce60722d9f1b722e17aea5bbc26f97095d Mon Sep 17 00:00:00 2001 From: N-010 Date: Wed, 26 Aug 2026 16:51:06 +0300 Subject: [PATCH 59/59] Update NOST_CONTRACT_INDEX migration version to 228 --- src/contract_core/contract_def.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 5c463a1c8..482d6a331 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 }, {NOST_CONTRACT_INDEX, MIGRATE, 227}}; +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]);