diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj
index 942c974b..9f1fdd81 100644
--- a/src/Qubic.vcxproj
+++ b/src/Qubic.vcxproj
@@ -44,6 +44,7 @@
+
diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters
index 160c5365..c31d9825 100644
--- a/src/Qubic.vcxproj.filters
+++ b/src/Qubic.vcxproj.filters
@@ -123,6 +123,9 @@
contracts
+
+ contracts
+
contracts
diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h
index 2aa6e074..ba0751e2 100644
--- a/src/contract_core/contract_def.h
+++ b/src/contract_core/contract_def.h
@@ -301,6 +301,16 @@
#define CONTRACT_STATE2_TYPE WOLFPACK2
#include "contracts/GGWP.h"
+#undef CONTRACT_INDEX
+#undef CONTRACT_STATE_TYPE
+#undef CONTRACT_STATE2_TYPE
+
+#define QTREAT_CONTRACT_INDEX 29
+#define CONTRACT_INDEX QTREAT_CONTRACT_INDEX
+#define CONTRACT_STATE_TYPE QTREAT
+#define CONTRACT_STATE2_TYPE QTREAT2
+#include "contracts/QTREAT.h"
+
// new contracts should be added above this line
#ifdef INCLUDE_CONTRACT_TEST_EXAMPLES
@@ -416,6 +426,7 @@ constexpr struct ContractDescription
{"QUSINO", 208, 10000, sizeof(QUSINO::StateData)}, // proposal in epoch 206, IPO in 207, construction and first use in 208
{"ESCROW", 210, 10000, sizeof(ESCROW::StateData)}, // proposal in epoch 208, IPO in 209, construction and first use in 210
{"GGWP", 218, 10000, sizeof(WOLFPACK::StateData)}, // proposal in epoch 216, IPO in 217, construction and first use in 218
+ {"QTREAT", 222, 10000, sizeof(QTREAT::StateData)}, // proposal in epoch 220, IPO in 221, construction and first use in 222
// new contracts should be added above this line
#ifdef INCLUDE_CONTRACT_TEST_EXAMPLES
{"TESTEXA", 138, 10000, sizeof(TESTEXA::StateData)},
@@ -548,6 +559,7 @@ static void initializeContracts()
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QUSINO);
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(ESCROW);
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(WOLFPACK);
+ REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QTREAT);
// new contracts should be added above this line
#ifdef INCLUDE_CONTRACT_TEST_EXAMPLES
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA);
diff --git a/src/contracts/QTREAT.h b/src/contracts/QTREAT.h
new file mode 100644
index 00000000..0206c7ca
--- /dev/null
+++ b/src/contracts/QTREAT.h
@@ -0,0 +1,1815 @@
+using namespace QPI;
+
+constexpr uint64 QTREAT_MAX_HOLDERS = 131072;
+constexpr uint64 QTREAT_MAX_STAKERS = 65536;
+constexpr uint64 QTREAT_MAX_ASSETS = 1024;
+constexpr uint64 QTREAT_MIN_ELIGIBLE_BAL = 1;
+
+constexpr uint64 QTREAT_TOKEN_ASSETNAME = 92639312630865ULL; // "QTREAT"
+constexpr uint64 QTREAT_QDOGE_ASSETNAME = 297549120593ULL; // "QDOGE"
+
+constexpr uint64 QTREAT_MIN_STAKE = 10000000ULL;
+constexpr uint64 QTREAT_STAKER_REWARD_PER_EPOCH = 20000000ULL;
+constexpr uint64 QTREAT_PHASE_EPOCHS = 52;
+constexpr uint64 QTREAT_NUM_PHASES = 1;
+constexpr uint64 QTREAT_TOTAL_REWARD_EPOCHS = QTREAT_PHASE_EPOCHS * QTREAT_NUM_PHASES;
+constexpr uint64 QTREAT_UNSTAKE_DELAY_EPOCHS = 2;
+
+constexpr uint64 QTREAT_PROGRESSIVE_MIN_STEP = 1000000ULL;
+constexpr uint64 QTREAT_PROGRESSIVE_BONUS_PERMILLE = 25;
+constexpr uint64 QTREAT_PROGRESSIVE_MAX_STREAK = 20;
+constexpr uint64 QTREAT_PROGRESSIVE_START_DELAY_EPOCHS = 4;
+
+constexpr uint64 QTREAT_BONUS_THRESHOLD = 50000000ULL;
+constexpr uint64 QTREAT_BONUS_INTERVAL_EPOCHS = 12;
+constexpr uint64 QTREAT_BONUS_MAX_PER_WALLET = 4;
+
+constexpr uint64 QTREAT_RAFFLE_EPOCHS = 52;
+constexpr uint16 QTREAT_RAFFLE_ENTROPY_BITS = 256;
+constexpr uint8 QTREAT_RAFFLE_COLLATERAL_TIER = 0;
+constexpr uint64 QTREAT_RAFFLE_ENTROPY_FEE = RANDOM_BITFEE * QTREAT_RAFFLE_ENTROPY_BITS;
+
+constexpr uint64 QTREAT_ASIC_CATALOG_CAPACITY = 512;
+constexpr uint64 QTREAT_ASIC_PARTS_TOTAL = 400;
+constexpr uint64 QTREAT_MAX_ASIC_RIGS = 128;
+constexpr uint64 QTREAT_MAX_TOTAL_ASICS = 100;
+// Ownership re-verification / possessor snapshotting is spread across this many epochs
+// (round-robin by slot index) instead of touching every entry every epoch, so a large
+// registered-rig or dividend-NFT set can't force hundreds of external QBAY calls into a
+// single END_EPOCH. Each entry is re-checked exactly once every N epochs, and is only ever
+// paid/credited in the same epoch it's freshly re-checked (payout amount scaled by N to
+// compensate) - a sold/transferred asset can never keep paying its previous owner between
+// checks, and a new owner starts earning as soon as their asset's next check lands.
+constexpr uint64 QTREAT_ASIC_VERIFY_SPREAD_EPOCHS = 8;
+constexpr uint64 QTREAT_NFT_SNAPSHOT_SPREAD_EPOCHS = 4;
+constexpr uint64 QTREAT_MINING_REWARD_DEFAULT = 20000000ULL;
+constexpr uint64 QTREAT_MINING_REWARD_MAX = 100000000ULL;
+static_assert(QTREAT_MINING_REWARD_DEFAULT <= QTREAT_MINING_REWARD_MAX);
+constexpr uint64 QTREAT_RARITY_POINTS_COMMON = 1;
+constexpr uint64 QTREAT_RARITY_POINTS_UNCOMMON = 2;
+constexpr uint64 QTREAT_RARITY_POINTS_RARE = 3;
+constexpr uint64 QTREAT_RARITY_POINTS_EPIC = 5;
+constexpr uint64 QTREAT_RARITY_POINTS_LEGENDARY = 8;
+constexpr uint64 QTREAT_MAX_RARITY = 4;
+constexpr uint64 QTREAT_RARITY_SUPPLY_COMMON = 40;
+constexpr uint64 QTREAT_RARITY_SUPPLY_UNCOMMON = 30;
+constexpr uint64 QTREAT_RARITY_SUPPLY_RARE = 18;
+constexpr uint64 QTREAT_RARITY_SUPPLY_EPIC = 10;
+constexpr uint64 QTREAT_RARITY_SUPPLY_LEGENDARY = 2;
+static_assert(QTREAT_RARITY_SUPPLY_COMMON + QTREAT_RARITY_SUPPLY_UNCOMMON + QTREAT_RARITY_SUPPLY_RARE
+ + QTREAT_RARITY_SUPPLY_EPIC + QTREAT_RARITY_SUPPLY_LEGENDARY == 100);
+static_assert(QTREAT_MAX_TOTAL_ASICS == 100);
+
+constexpr uint64 QTREAT_DRIP_EPOCHS = 52;
+constexpr uint64 QTREAT_DRIP_QDOGE_COMMON = 10000;
+constexpr uint64 QTREAT_DRIP_QDOGE_UNCOMMON = 20000;
+constexpr uint64 QTREAT_DRIP_QDOGE_RARE = 30000;
+constexpr uint64 QTREAT_DRIP_QDOGE_EPIC = 50000;
+constexpr uint64 QTREAT_DRIP_QDOGE_LEGENDARY = 100000;
+
+constexpr uint64 QTREAT_DIVIDEND_SHAREHOLDER_PERMILLE = 50;
+static_assert(QTREAT_DIVIDEND_SHAREHOLDER_PERMILLE < 1000);
+
+constexpr uint64 QTREAT_MAX_NFT_HOLDERS = 1024;
+constexpr uint64 QTREAT_MAX_DIVIDEND_NFTS = 256;
+
+constexpr uint64 QTREAT_MAX_EXCLUDE_ADDRESSES = 4;
+
+constexpr uint16 QTREAT_QX_CONTRACT_INDEX = 1;
+constexpr sint64 QTREAT_QX_TRANSFER_FEE = 100LL;
+
+constexpr uint32 QTREAT_OK = 0;
+constexpr uint32 QTREAT_ERR_ACCESS_DENIED = 1;
+constexpr uint32 QTREAT_ERR_ZERO_AMOUNT = 2;
+constexpr uint32 QTREAT_ERR_INSUFFICIENT_STAKE = 3;
+constexpr uint32 QTREAT_ERR_UNSTAKE_PENDING = 4;
+constexpr uint32 QTREAT_ERR_UNSTAKE_NOT_READY = 5;
+constexpr uint32 QTREAT_ERR_NOT_STAKER = 6;
+constexpr uint32 QTREAT_ERR_BELOW_MIN_STAKE = 7;
+constexpr uint32 QTREAT_ERR_ACQUIRE_FAILED = 8;
+constexpr uint32 QTREAT_ERR_TRANSFER_FAILED = 9;
+constexpr uint32 QTREAT_ERR_NO_PENDING_BONUS = 10;
+constexpr uint32 QTREAT_ERR_INSUFFICIENT_FEE = 11;
+constexpr uint32 QTREAT_ERR_INVALID_INPUT = 12;
+constexpr uint32 QTREAT_ERR_PHASES_ENDED = 13;
+constexpr uint32 QTREAT_ERR_BONUS_POOL_EMPTY = 14;
+constexpr uint32 QTREAT_ERR_EXTERNAL_CALL = 15;
+
+struct QTREAT2 {};
+
+struct QTREAT : public ContractBase
+{
+ struct StakerInfo
+ {
+ uint64 staked;
+ uint64 unstakeAmount;
+ uint64 unstakeEpoch;
+ uint64 bonusEpochs;
+ uint64 hwmHoldings;
+ uint64 lastStaked;
+ uint64 growthStreak;
+ uint64 bonusAwarded;
+ uint64 pendingBonus;
+ };
+
+ struct RaffleSeed
+ {
+ bit_4096 entropy;
+ id prevSpectrumDigest;
+ id prevUniverseDigest;
+ id prevComputerDigest;
+ uint64 epoch;
+ uint64 tick;
+ uint64 eligibleCount;
+ };
+
+ struct AsicRig
+ {
+ id owner;
+ uint32 partMotherboard;
+ uint32 partChip;
+ uint32 partPsu;
+ uint32 partFan;
+ uint64 weight;
+ uint64 active;
+ };
+
+ struct AssetKey
+ {
+ id issuer;
+ uint64 assetName;
+ bool operator==(const AssetKey& o) const { return issuer == o.issuer && assetName == o.assetName; }
+ bool operator!=(const AssetKey& o) const { return issuer != o.issuer || assetName != o.assetName; }
+ };
+
+ struct StateData
+ {
+ id adminAddress;
+
+ Asset qtreatToken;
+ Asset qdogeToken;
+
+ uint64 dividendFund;
+ uint64 totalDividendsDistributed;
+
+ uint64 stakingFund;
+ uint64 totalStakingRewardsDistributed;
+
+ HashMap beginBalances;
+ HashMap endBalances;
+ uint64 totalHoldersSnapshot;
+
+ HashMap stakers;
+ uint64 totalStaked;
+ uint64 stakingStartEpoch;
+
+ uint64 qtreatBonusPool;
+
+ Array excludeAddresses;
+
+ Array dividendNftIds;
+ uint64 dividendNftIdCount;
+ HashMap nftCounts;
+ uint64 totalNftCount;
+
+ uint64 totalShareholderDividends;
+
+ HashMap asicCatalog;
+ Array asicCatalogBuckets;
+ uint64 asicCatalogSize;
+ uint64 asicCatalogLocked;
+ HashMap asicUsedParts;
+ Array asicRigs;
+ uint64 asicRigHighWater;
+ uint64 totalAsicCount;
+ uint64 totalMiningWeight;
+ uint64 miningFund;
+ uint64 miningRewardRate;
+ uint64 totalMiningRewardsDistributed;
+
+ uint64 dripQdogePool;
+ uint64 dripStartEpoch;
+ uint64 totalDripQdogeDistributed;
+ HashMap dripTally;
+
+ uint64 totalBonusDelivered;
+
+ uint64 totalRaffleAwarded;
+ id lastRaffleWinner;
+ uint64 lastRaffleEpoch;
+
+ HashMap generalAssetBalances;
+ HashMap scDividendTracker;
+ };
+
+ struct DepositDividends_input {}; struct DepositDividends_output { uint32 returnCode; };
+ struct DepositStakingFund_input {}; struct DepositStakingFund_output { uint32 returnCode; };
+
+ struct RequestUnstake_input { uint64 amount; }; struct RequestUnstake_output { uint32 returnCode; };
+ struct RequestUnstake_locals { StakerInfo info; };
+ struct FinalizeUnstake_input {}; struct FinalizeUnstake_output { uint32 returnCode; };
+ struct FinalizeUnstake_locals { StakerInfo info; sint64 rel; };
+
+ struct ClaimQtreatBonus_input {}; struct ClaimQtreatBonus_output { uint32 returnCode; uint64 claimed; };
+ struct ClaimQtreatBonus_locals { StakerInfo info; sint64 xfer; sint64 rel; };
+
+ struct DepositQtreatTokens_input { uint64 amount; }; struct DepositQtreatTokens_output { uint32 returnCode; };
+ struct DepositQtreatTokens_locals { sint64 xfer; };
+
+ struct DepositGeneralAsset_input { Asset asset; uint64 amount; };
+ struct DepositGeneralAsset_output { uint32 returnCode; };
+ struct DepositGeneralAsset_locals { sint64 managed; sint64 xfer; AssetKey key; uint64 bal; };
+
+ struct RevokeGeneralAsset_input { Asset asset; uint64 amount; };
+ struct RevokeGeneralAsset_output { uint32 returnCode; };
+ struct RevokeGeneralAsset_locals { AssetKey key; uint64 bal; sint64 xfer; sint64 rel; };
+
+ struct ReleaseManagedShares_input { Asset asset; uint64 amount; };
+ struct ReleaseManagedShares_output { uint32 returnCode; };
+ struct ReleaseManagedShares_locals { sint64 managed; sint64 rel; StakerInfo info; uint64 accounted; };
+
+ struct SetExcludeAddress_input { uint64 slot; id address; };
+ struct SetExcludeAddress_output { uint32 returnCode; };
+
+ struct DepositMiningFund_input {}; struct DepositMiningFund_output { uint32 returnCode; };
+
+ struct SetMiningRate_input { uint64 ratePerEpoch; };
+ struct SetMiningRate_output { uint32 returnCode; };
+
+ struct DepositDripQdoge_input { uint64 amount; };
+ struct DepositDripQdoge_output { uint32 returnCode; };
+ struct DepositDripQdoge_locals { sint64 xfer; };
+
+ struct LoadAsicPart_input { uint32 nftId; uint8 category; uint8 rarity; };
+ struct LoadAsicPart_output { uint32 returnCode; uint64 catalogSize; uint64 locked; };
+ struct LoadAsicPart_locals { uint64 budget; uint64 dummy; uint64 partPoints; };
+
+ struct RegisterAsic_input { uint32 partMotherboard; uint32 partChip; uint32 partPsu; uint32 partFan; };
+ struct RegisterAsic_output { uint32 returnCode; uint64 rigIndex; uint64 asicWeight; };
+ struct RegisterAsic_locals
+ {
+ AsicRig rig; uint64 points; uint64 partPoints; uint64 packed; uint64 r; sint64 i; uint32 partId; uint64 dummy;
+ QBAY::getInfoOfNFTById_input qbayIn;
+ QBAY::getInfoOfNFTById_output qbayOut;
+ sint64 freeSlot;
+ };
+
+ struct UnregisterAsic_input { uint64 rigIndex; };
+ struct UnregisterAsic_output { uint32 returnCode; };
+ struct UnregisterAsic_locals { AsicRig rig; };
+
+ struct GetMinerInfo_input { id wallet; };
+ struct GetMinerInfo_output { uint64 asicCount; uint64 weight; uint64 totalAsicCount; uint64 totalMiningWeight; uint64 miningFund; };
+ struct GetMinerInfo_locals { AsicRig rig; sint64 i; };
+
+ struct GetAsicCatalogInfo_input { uint32 nftId; };
+ struct GetAsicCatalogInfo_output { uint64 catalogSize; uint64 locked; uint64 isCataloged; uint64 category; uint64 rarity; uint64 usedByRigPlusOne; };
+ struct GetAsicCatalogInfo_locals { uint64 packed; uint64 used; };
+
+ struct GetNftInfo_input { id wallet; };
+ struct GetNftInfo_output { uint64 nftCount; uint64 totalNftCount; };
+ struct GetNftInfo_locals { uint64 val; };
+
+ struct GetExcludeAddresses_input {};
+ struct GetExcludeAddresses_output { Array addresses; };
+ struct GetExcludeAddresses_locals { sint64 i; };
+
+ struct GetStakingInfo_input { id staker; };
+ struct GetStakingInfo_output
+ {
+ uint64 staked; uint64 unstakeAmount; uint64 unstakeEpoch;
+ uint64 bonusEpochs; uint64 bonusAwarded; uint64 pendingBonus;
+ uint64 growthStreak; uint64 hwmHoldings;
+ uint64 totalStaked; uint64 stakingFund; uint32 isStaker;
+ };
+ struct GetStakingInfo_locals { StakerInfo info; };
+
+ struct GetPhaseInfo_input {};
+ struct GetPhaseInfo_output
+ {
+ uint64 stakingStartEpoch;
+ uint64 currentPhase;
+ uint64 epochsRemaining;
+ };
+ struct GetPhaseInfo_locals { uint64 elapsed; };
+
+ struct GetRaffleInfo_input {};
+ struct GetRaffleInfo_output
+ {
+ uint64 totalRaffleAwarded;
+ id lastRaffleWinner;
+ uint64 lastRaffleEpoch;
+ uint64 raffleEpochsRemaining;
+ };
+ struct GetRaffleInfo_locals { uint64 elapsed; };
+
+ struct GetFunds_input {};
+ struct GetFunds_output
+ {
+ uint64 dividendFund; uint64 stakingFund; uint64 qtreatBonusPool;
+ uint64 totalDividendsDistributed; uint64 totalStakingRewardsDistributed;
+ uint64 totalBonusDelivered;
+ uint64 totalShareholderDividends;
+ uint64 miningFund;
+ uint64 miningRewardRate;
+ uint64 totalMiningRewardsDistributed;
+ uint64 dripQdogePool;
+ uint64 dripStartEpoch;
+ uint64 totalDripQdogeDistributed;
+ };
+
+ PUBLIC_FUNCTION_WITH_LOCALS(GetStakingInfo)
+ {
+ output.isStaker = state.get().stakers.get(input.staker, locals.info) ? 1 : 0;
+ if (output.isStaker)
+ {
+ output.staked = locals.info.staked;
+ output.unstakeAmount = locals.info.unstakeAmount;
+ output.unstakeEpoch = locals.info.unstakeEpoch;
+ output.bonusEpochs = locals.info.bonusEpochs;
+ output.bonusAwarded = locals.info.bonusAwarded;
+ output.pendingBonus = locals.info.pendingBonus;
+ output.growthStreak = locals.info.growthStreak;
+ output.hwmHoldings = locals.info.hwmHoldings;
+ }
+ output.totalStaked = state.get().totalStaked;
+ output.stakingFund = state.get().stakingFund;
+ }
+
+ PUBLIC_FUNCTION_WITH_LOCALS(GetPhaseInfo)
+ {
+ output.stakingStartEpoch = state.get().stakingStartEpoch;
+ output.currentPhase = 0;
+ output.epochsRemaining = 0;
+ if (state.get().stakingStartEpoch == 0) return;
+ locals.elapsed = qpi.epoch() - state.get().stakingStartEpoch;
+ if (locals.elapsed < QTREAT_TOTAL_REWARD_EPOCHS)
+ {
+ output.currentPhase = div(locals.elapsed, QTREAT_PHASE_EPOCHS) + 1;
+ output.epochsRemaining = QTREAT_TOTAL_REWARD_EPOCHS - locals.elapsed;
+ }
+ }
+
+ PUBLIC_FUNCTION_WITH_LOCALS(GetRaffleInfo)
+ {
+ output.totalRaffleAwarded = state.get().totalRaffleAwarded;
+ output.lastRaffleWinner = state.get().lastRaffleWinner;
+ output.lastRaffleEpoch = state.get().lastRaffleEpoch;
+ output.raffleEpochsRemaining = 0;
+ if (state.get().stakingStartEpoch != 0)
+ {
+ locals.elapsed = qpi.epoch() - state.get().stakingStartEpoch;
+ if (locals.elapsed < QTREAT_RAFFLE_EPOCHS)
+ {
+ output.raffleEpochsRemaining = QTREAT_RAFFLE_EPOCHS - locals.elapsed;
+ }
+ }
+ }
+
+ PUBLIC_FUNCTION_WITH_LOCALS(GetMinerInfo)
+ {
+ for (locals.i = 0; locals.i < (sint64)state.get().asicRigHighWater; locals.i++)
+ {
+ locals.rig = state.get().asicRigs.get(locals.i);
+ if (locals.rig.active == 0 || locals.rig.owner != input.wallet) continue;
+ output.asicCount = output.asicCount + 1;
+ output.weight = output.weight + locals.rig.weight;
+ }
+ output.totalAsicCount = state.get().totalAsicCount;
+ output.totalMiningWeight = state.get().totalMiningWeight;
+ output.miningFund = state.get().miningFund;
+ }
+
+ PUBLIC_FUNCTION_WITH_LOCALS(GetAsicCatalogInfo)
+ {
+ output.catalogSize = state.get().asicCatalogSize;
+ output.locked = state.get().asicCatalogLocked;
+ locals.packed = 0;
+ output.isCataloged = state.get().asicCatalog.get((uint64)input.nftId, locals.packed) ? 1 : 0;
+ if (output.isCataloged)
+ {
+ output.category = div(locals.packed, 8ULL);
+ output.rarity = mod(locals.packed, 8ULL);
+ }
+ locals.used = 0;
+ state.get().asicUsedParts.get((uint64)input.nftId, locals.used);
+ output.usedByRigPlusOne = locals.used;
+ }
+
+ PUBLIC_FUNCTION_WITH_LOCALS(GetNftInfo)
+ {
+ locals.val = 0;
+ state.get().nftCounts.get(input.wallet, locals.val);
+ output.nftCount = locals.val;
+ output.totalNftCount = state.get().totalNftCount;
+ }
+
+ PUBLIC_FUNCTION_WITH_LOCALS(GetExcludeAddresses)
+ {
+ for (locals.i = 0; locals.i < (sint64)QTREAT_MAX_EXCLUDE_ADDRESSES; locals.i++)
+ {
+ output.addresses.set(locals.i, state.get().excludeAddresses.get(locals.i));
+ }
+ }
+
+ PUBLIC_FUNCTION(GetFunds)
+ {
+ output.dividendFund = state.get().dividendFund;
+ output.stakingFund = state.get().stakingFund;
+ output.qtreatBonusPool = state.get().qtreatBonusPool;
+ output.totalDividendsDistributed = state.get().totalDividendsDistributed;
+ output.totalStakingRewardsDistributed = state.get().totalStakingRewardsDistributed;
+ output.totalBonusDelivered = state.get().totalBonusDelivered;
+ output.totalShareholderDividends = state.get().totalShareholderDividends;
+ output.miningFund = state.get().miningFund;
+ output.miningRewardRate = state.get().miningRewardRate;
+ output.totalMiningRewardsDistributed = state.get().totalMiningRewardsDistributed;
+ output.dripQdogePool = state.get().dripQdogePool;
+ output.dripStartEpoch = state.get().dripStartEpoch;
+ output.totalDripQdogeDistributed = state.get().totalDripQdogeDistributed;
+ }
+
+ PUBLIC_PROCEDURE(DepositDividends)
+ {
+ if (qpi.invocationReward() == 0) { output.returnCode = QTREAT_ERR_ZERO_AMOUNT; return; }
+ state.mut().dividendFund = sadd(state.get().dividendFund, (uint64)qpi.invocationReward());
+ output.returnCode = QTREAT_OK;
+ }
+
+ PUBLIC_PROCEDURE(DepositStakingFund)
+ {
+ if (qpi.invocationReward() == 0) { output.returnCode = QTREAT_ERR_ZERO_AMOUNT; return; }
+ state.mut().stakingFund = sadd(state.get().stakingFund, (uint64)qpi.invocationReward());
+ output.returnCode = QTREAT_OK;
+ }
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(RequestUnstake)
+ {
+ if (!state.get().stakers.get(qpi.invocator(), locals.info) || locals.info.staked == 0)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.returnCode = QTREAT_ERR_NOT_STAKER; return;
+ }
+ if (input.amount == 0 || input.amount > locals.info.staked)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.returnCode = QTREAT_ERR_INSUFFICIENT_STAKE; return;
+ }
+ if (locals.info.unstakeAmount > 0)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.returnCode = QTREAT_ERR_UNSTAKE_PENDING; return;
+ }
+ if (input.amount < locals.info.staked && locals.info.staked - input.amount < QTREAT_MIN_STAKE)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.returnCode = QTREAT_ERR_BELOW_MIN_STAKE; return;
+ }
+ if (qpi.invocationReward() < QTREAT_QX_TRANSFER_FEE)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.returnCode = QTREAT_ERR_INSUFFICIENT_FEE; return;
+ }
+ if (qpi.invocationReward() > QTREAT_QX_TRANSFER_FEE)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward() - QTREAT_QX_TRANSFER_FEE);
+ }
+
+ locals.info.staked -= input.amount;
+ locals.info.unstakeAmount = input.amount;
+ locals.info.unstakeEpoch = qpi.epoch();
+ if (locals.info.staked == 0)
+ {
+ locals.info.bonusEpochs = 0;
+ locals.info.growthStreak = 0;
+ locals.info.lastStaked = 0;
+ }
+ state.mut().stakers.set(qpi.invocator(), locals.info);
+ state.mut().totalStaked = state.get().totalStaked - input.amount;
+ output.returnCode = QTREAT_OK;
+ }
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(FinalizeUnstake)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ if (!state.get().stakers.get(qpi.invocator(), locals.info) || locals.info.unstakeAmount == 0)
+ {
+ output.returnCode = QTREAT_ERR_NOT_STAKER; return;
+ }
+ if (qpi.epoch() < locals.info.unstakeEpoch + QTREAT_UNSTAKE_DELAY_EPOCHS)
+ {
+ output.returnCode = QTREAT_ERR_UNSTAKE_NOT_READY; return;
+ }
+
+ locals.rel = qpi.releaseShares(state.get().qdogeToken, qpi.invocator(), qpi.invocator(),
+ (sint64)locals.info.unstakeAmount, QTREAT_QX_CONTRACT_INDEX, QTREAT_QX_CONTRACT_INDEX,
+ QTREAT_QX_TRANSFER_FEE);
+ if (locals.rel < 0) { output.returnCode = QTREAT_ERR_TRANSFER_FAILED; return; }
+
+ locals.info.unstakeAmount = 0;
+ locals.info.unstakeEpoch = 0;
+ if (locals.info.staked == 0 && locals.info.pendingBonus == 0
+ && locals.info.bonusAwarded == 0)
+ {
+ state.mut().stakers.removeByKey(qpi.invocator());
+ }
+ else
+ {
+ state.mut().stakers.set(qpi.invocator(), locals.info);
+ }
+ output.returnCode = QTREAT_OK;
+ }
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(DepositQtreatTokens)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ if (qpi.invocator() != state.get().adminAddress) { output.returnCode = QTREAT_ERR_ACCESS_DENIED; return; }
+ if (input.amount == 0) { output.returnCode = QTREAT_ERR_ZERO_AMOUNT; return; }
+ locals.xfer = qpi.transferShareOwnershipAndPossession(
+ state.get().qtreatToken.assetName, state.get().qtreatToken.issuer,
+ qpi.invocator(), qpi.invocator(), (sint64)input.amount, SELF);
+ if (locals.xfer < 0) { output.returnCode = QTREAT_ERR_ACQUIRE_FAILED; return; }
+ state.mut().qtreatBonusPool = sadd(state.get().qtreatBonusPool, input.amount);
+ output.returnCode = QTREAT_OK;
+ }
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(ClaimQtreatBonus)
+ {
+ if (!state.get().stakers.get(qpi.invocator(), locals.info) || locals.info.pendingBonus == 0)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.returnCode = QTREAT_ERR_NO_PENDING_BONUS; return;
+ }
+ if (state.get().qtreatBonusPool < locals.info.pendingBonus)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.returnCode = QTREAT_ERR_BONUS_POOL_EMPTY; return;
+ }
+
+ if (qpi.invocationReward() < QTREAT_QX_TRANSFER_FEE)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.returnCode = QTREAT_ERR_INSUFFICIENT_FEE;
+ return;
+ }
+ if (qpi.invocationReward() > QTREAT_QX_TRANSFER_FEE)
+ qpi.transfer(qpi.invocator(), qpi.invocationReward() - QTREAT_QX_TRANSFER_FEE);
+
+ locals.xfer = qpi.transferShareOwnershipAndPossession(
+ state.get().qtreatToken.assetName, state.get().qtreatToken.issuer,
+ SELF, SELF, (sint64)locals.info.pendingBonus, qpi.invocator());
+ if (locals.xfer < 0) { output.returnCode = QTREAT_ERR_TRANSFER_FAILED; return; }
+
+ state.mut().qtreatBonusPool = state.get().qtreatBonusPool - locals.info.pendingBonus;
+ output.claimed = locals.info.pendingBonus;
+ locals.info.pendingBonus = 0;
+ state.mut().stakers.set(qpi.invocator(), locals.info);
+
+ locals.rel = qpi.releaseShares(state.get().qtreatToken, qpi.invocator(), qpi.invocator(),
+ (sint64)output.claimed, QTREAT_QX_CONTRACT_INDEX, QTREAT_QX_CONTRACT_INDEX,
+ QTREAT_QX_TRANSFER_FEE);
+ output.returnCode = QTREAT_OK;
+ }
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(DepositGeneralAsset)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ if (qpi.invocator() != state.get().adminAddress) { output.returnCode = QTREAT_ERR_ACCESS_DENIED; return; }
+ if (input.amount == 0 || input.asset.issuer == NULL_ID) { output.returnCode = QTREAT_ERR_INVALID_INPUT; return; }
+
+ locals.managed = qpi.numberOfShares(input.asset,
+ { qpi.invocator(), SELF_INDEX }, { qpi.invocator(), SELF_INDEX });
+ if (locals.managed < (sint64)input.amount) { output.returnCode = QTREAT_ERR_ACQUIRE_FAILED; return; }
+
+ locals.key.issuer = input.asset.issuer;
+ locals.key.assetName = input.asset.assetName;
+ if (!state.get().generalAssetBalances.contains(locals.key)
+ && state.get().generalAssetBalances.population() >= QTREAT_MAX_ASSETS)
+ {
+ output.returnCode = QTREAT_ERR_INVALID_INPUT; return;
+ }
+
+ locals.xfer = qpi.transferShareOwnershipAndPossession(
+ input.asset.assetName, input.asset.issuer,
+ qpi.invocator(), qpi.invocator(), (sint64)input.amount, SELF);
+ if (locals.xfer < 0) { output.returnCode = QTREAT_ERR_TRANSFER_FAILED; return; }
+
+ locals.bal = 0;
+ state.get().generalAssetBalances.get(locals.key, locals.bal);
+ state.mut().generalAssetBalances.set(locals.key, sadd(locals.bal, input.amount));
+ output.returnCode = QTREAT_OK;
+ }
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(ReleaseManagedShares)
+ {
+ locals.managed = qpi.numberOfShares(input.asset,
+ { qpi.invocator(), SELF_INDEX }, { qpi.invocator(), SELF_INDEX });
+ if (input.amount == 0 || locals.managed < (sint64)input.amount)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.returnCode = QTREAT_ERR_INVALID_INPUT; return;
+ }
+ if (input.asset.assetName == state.get().qdogeToken.assetName
+ && input.asset.issuer == state.get().qdogeToken.issuer)
+ {
+ locals.info.staked = 0; locals.info.unstakeAmount = 0;
+ state.get().stakers.get(qpi.invocator(), locals.info);
+ locals.accounted = sadd(locals.info.staked, locals.info.unstakeAmount);
+ if ((sint64)locals.accounted + (sint64)input.amount > locals.managed)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.returnCode = QTREAT_ERR_INVALID_INPUT; return;
+ }
+ }
+ if (qpi.invocationReward() < QTREAT_QX_TRANSFER_FEE)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.returnCode = QTREAT_ERR_INSUFFICIENT_FEE; return;
+ }
+ if (qpi.invocationReward() > QTREAT_QX_TRANSFER_FEE)
+ qpi.transfer(qpi.invocator(), qpi.invocationReward() - QTREAT_QX_TRANSFER_FEE);
+
+ locals.rel = qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(),
+ (sint64)input.amount, QTREAT_QX_CONTRACT_INDEX, QTREAT_QX_CONTRACT_INDEX,
+ QTREAT_QX_TRANSFER_FEE);
+ if (locals.rel < 0) { output.returnCode = QTREAT_ERR_TRANSFER_FAILED; return; }
+ output.returnCode = QTREAT_OK;
+ }
+
+ PUBLIC_PROCEDURE(DepositMiningFund)
+ {
+ if (qpi.invocationReward() == 0) { output.returnCode = QTREAT_ERR_ZERO_AMOUNT; return; }
+ state.mut().miningFund = sadd(state.get().miningFund, (uint64)qpi.invocationReward());
+ output.returnCode = QTREAT_OK;
+ }
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(LoadAsicPart)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ if (qpi.invocator() != state.get().adminAddress) { output.returnCode = QTREAT_ERR_ACCESS_DENIED; return; }
+ if (state.get().asicCatalogLocked != 0) { output.returnCode = QTREAT_ERR_INVALID_INPUT; return; }
+ if (input.category >= 4 || input.rarity > QTREAT_MAX_RARITY) { output.returnCode = QTREAT_ERR_INVALID_INPUT; return; }
+ if (state.get().asicCatalog.contains((uint64)input.nftId)) { output.returnCode = QTREAT_ERR_INVALID_INPUT; return; }
+
+ locals.budget = QTREAT_RARITY_SUPPLY_COMMON;
+ if (input.rarity == 1) locals.budget = QTREAT_RARITY_SUPPLY_UNCOMMON;
+ if (input.rarity == 2) locals.budget = QTREAT_RARITY_SUPPLY_RARE;
+ if (input.rarity == 3) locals.budget = QTREAT_RARITY_SUPPLY_EPIC;
+ if (input.rarity == 4) locals.budget = QTREAT_RARITY_SUPPLY_LEGENDARY;
+ if (state.get().asicCatalogBuckets.get((sint64)input.category * 8 + (sint64)input.rarity) >= locals.budget)
+ {
+ output.returnCode = QTREAT_ERR_INVALID_INPUT; return;
+ }
+
+ if (state.mut().asicCatalog.set((uint64)input.nftId, (uint64)input.category * 8 + (uint64)input.rarity) == NULL_INDEX)
+ {
+ output.returnCode = QTREAT_ERR_INVALID_INPUT; return;
+ }
+ state.mut().asicCatalogBuckets.set((sint64)input.category * 8 + (sint64)input.rarity,
+ state.get().asicCatalogBuckets.get((sint64)input.category * 8 + (sint64)input.rarity) + 1);
+ state.mut().asicCatalogSize = state.get().asicCatalogSize + 1;
+ if (state.get().asicCatalogSize == QTREAT_ASIC_PARTS_TOTAL)
+ {
+ state.mut().asicCatalogLocked = 1;
+ state.mut().dripStartEpoch = qpi.epoch();
+ }
+ output.catalogSize = state.get().asicCatalogSize;
+ output.locked = state.get().asicCatalogLocked;
+ output.returnCode = QTREAT_OK;
+ }
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(RegisterAsic)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ if (state.get().asicCatalogLocked == 0) { output.returnCode = QTREAT_ERR_INVALID_INPUT; return; }
+ if (state.get().totalAsicCount >= QTREAT_MAX_TOTAL_ASICS) { output.returnCode = QTREAT_ERR_INVALID_INPUT; return; }
+
+ locals.points = 0;
+ for (locals.i = 0; locals.i < 4; locals.i++)
+ {
+ if (locals.i == 0) locals.partId = input.partMotherboard;
+ if (locals.i == 1) locals.partId = input.partChip;
+ if (locals.i == 2) locals.partId = input.partPsu;
+ if (locals.i == 3) locals.partId = input.partFan;
+
+ locals.packed = 0;
+ if (!state.get().asicCatalog.get((uint64)locals.partId, locals.packed)
+ || div(locals.packed, 8ULL) != (uint64)locals.i)
+ {
+ output.returnCode = QTREAT_ERR_INVALID_INPUT; return;
+ }
+ locals.dummy = 0;
+ if (state.get().asicUsedParts.get((uint64)locals.partId, locals.dummy))
+ {
+ output.returnCode = QTREAT_ERR_INVALID_INPUT; return;
+ }
+ locals.qbayIn.NFTId = locals.partId;
+ CALL_OTHER_CONTRACT_FUNCTION(QBAY, getInfoOfNFTById, locals.qbayIn, locals.qbayOut);
+ if (interContractCallError != NoCallError)
+ {
+ output.returnCode = QTREAT_ERR_EXTERNAL_CALL; return;
+ }
+ if (locals.qbayOut.possessor != qpi.invocator())
+ {
+ output.returnCode = QTREAT_ERR_ACCESS_DENIED; return;
+ }
+
+ locals.r = mod(locals.packed, 8ULL);
+ locals.partPoints = QTREAT_RARITY_POINTS_COMMON;
+ if (locals.r == 1) locals.partPoints = QTREAT_RARITY_POINTS_UNCOMMON;
+ if (locals.r == 2) locals.partPoints = QTREAT_RARITY_POINTS_RARE;
+ if (locals.r == 3) locals.partPoints = QTREAT_RARITY_POINTS_EPIC;
+ if (locals.r == 4) locals.partPoints = QTREAT_RARITY_POINTS_LEGENDARY;
+ locals.points += locals.partPoints;
+ }
+
+ locals.freeSlot = -1;
+ for (locals.i = 0; locals.i < (sint64)state.get().asicRigHighWater; locals.i++)
+ {
+ if (state.get().asicRigs.get(locals.i).active == 0) { locals.freeSlot = locals.i; break; }
+ }
+ if (locals.freeSlot < 0)
+ {
+ if (state.get().asicRigHighWater >= QTREAT_MAX_ASIC_RIGS)
+ {
+ output.returnCode = QTREAT_ERR_INVALID_INPUT; return;
+ }
+ locals.freeSlot = (sint64)state.get().asicRigHighWater;
+ state.mut().asicRigHighWater = state.get().asicRigHighWater + 1;
+ }
+
+ locals.rig.owner = qpi.invocator();
+ locals.rig.partMotherboard = input.partMotherboard;
+ locals.rig.partChip = input.partChip;
+ locals.rig.partPsu = input.partPsu;
+ locals.rig.partFan = input.partFan;
+ locals.rig.weight = locals.points;
+ locals.rig.active = 1;
+ state.mut().asicRigs.set(locals.freeSlot, locals.rig);
+ state.mut().asicUsedParts.set((uint64)input.partMotherboard, (uint64)locals.freeSlot + 1);
+ state.mut().asicUsedParts.set((uint64)input.partChip, (uint64)locals.freeSlot + 1);
+ state.mut().asicUsedParts.set((uint64)input.partPsu, (uint64)locals.freeSlot + 1);
+ state.mut().asicUsedParts.set((uint64)input.partFan, (uint64)locals.freeSlot + 1);
+ state.mut().totalAsicCount = state.get().totalAsicCount + 1;
+ state.mut().totalMiningWeight = sadd(state.get().totalMiningWeight, locals.points);
+
+ output.rigIndex = (uint64)locals.freeSlot;
+ output.asicWeight = locals.points;
+ output.returnCode = QTREAT_OK;
+ }
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(UnregisterAsic)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ if (input.rigIndex >= state.get().asicRigHighWater) { output.returnCode = QTREAT_ERR_INVALID_INPUT; return; }
+ locals.rig = state.get().asicRigs.get((sint64)input.rigIndex);
+ if (locals.rig.active == 0 || locals.rig.owner != qpi.invocator())
+ {
+ output.returnCode = QTREAT_ERR_ACCESS_DENIED; return;
+ }
+ state.mut().asicUsedParts.removeByKey((uint64)locals.rig.partMotherboard);
+ state.mut().asicUsedParts.removeByKey((uint64)locals.rig.partChip);
+ state.mut().asicUsedParts.removeByKey((uint64)locals.rig.partPsu);
+ state.mut().asicUsedParts.removeByKey((uint64)locals.rig.partFan);
+ state.mut().totalAsicCount = state.get().totalAsicCount - 1;
+ state.mut().totalMiningWeight = state.get().totalMiningWeight - locals.rig.weight;
+ locals.rig.active = 0;
+ state.mut().asicRigs.set((sint64)input.rigIndex, locals.rig);
+ output.returnCode = QTREAT_OK;
+ }
+
+ PUBLIC_PROCEDURE(SetMiningRate)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ if (qpi.invocator() != state.get().adminAddress) { output.returnCode = QTREAT_ERR_ACCESS_DENIED; return; }
+ if (input.ratePerEpoch > QTREAT_MINING_REWARD_MAX) { output.returnCode = QTREAT_ERR_INVALID_INPUT; return; }
+ state.mut().miningRewardRate = input.ratePerEpoch;
+ output.returnCode = QTREAT_OK;
+ }
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(DepositDripQdoge)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ if (qpi.invocator() != state.get().adminAddress) { output.returnCode = QTREAT_ERR_ACCESS_DENIED; return; }
+ if (input.amount == 0) { output.returnCode = QTREAT_ERR_ZERO_AMOUNT; return; }
+ locals.xfer = qpi.transferShareOwnershipAndPossession(
+ state.get().qdogeToken.assetName, state.get().qdogeToken.issuer,
+ qpi.invocator(), qpi.invocator(), (sint64)input.amount, SELF);
+ if (locals.xfer < 0) { output.returnCode = QTREAT_ERR_ACQUIRE_FAILED; return; }
+ state.mut().dripQdogePool = sadd(state.get().dripQdogePool, input.amount);
+ output.returnCode = QTREAT_OK;
+ }
+
+ PUBLIC_PROCEDURE(SetExcludeAddress)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ if (qpi.invocator() != state.get().adminAddress) { output.returnCode = QTREAT_ERR_ACCESS_DENIED; return; }
+ if (input.slot >= QTREAT_MAX_EXCLUDE_ADDRESSES) { output.returnCode = QTREAT_ERR_INVALID_INPUT; return; }
+ state.mut().excludeAddresses.set((sint64)input.slot, input.address);
+ output.returnCode = QTREAT_OK;
+ }
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(RevokeGeneralAsset)
+ {
+ if (qpi.invocator() != state.get().adminAddress)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.returnCode = QTREAT_ERR_ACCESS_DENIED; return;
+ }
+ locals.key.issuer = input.asset.issuer;
+ locals.key.assetName = input.asset.assetName;
+ locals.bal = 0;
+ state.get().generalAssetBalances.get(locals.key, locals.bal);
+ if (input.amount == 0 || input.amount > locals.bal)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.returnCode = QTREAT_ERR_INVALID_INPUT; return;
+ }
+ if (qpi.invocationReward() < QTREAT_QX_TRANSFER_FEE)
+ {
+ if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.returnCode = QTREAT_ERR_INSUFFICIENT_FEE; return;
+ }
+ if (qpi.invocationReward() > QTREAT_QX_TRANSFER_FEE)
+ qpi.transfer(qpi.invocator(), qpi.invocationReward() - QTREAT_QX_TRANSFER_FEE);
+
+ locals.xfer = qpi.transferShareOwnershipAndPossession(
+ input.asset.assetName, input.asset.issuer,
+ SELF, SELF, (sint64)input.amount, qpi.invocator());
+ if (locals.xfer < 0) { output.returnCode = QTREAT_ERR_TRANSFER_FAILED; return; }
+ locals.rel = qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(),
+ (sint64)input.amount, QTREAT_QX_CONTRACT_INDEX, QTREAT_QX_CONTRACT_INDEX,
+ QTREAT_QX_TRANSFER_FEE);
+ if (locals.rel < 0) { output.returnCode = QTREAT_ERR_TRANSFER_FAILED; return; }
+
+ if (locals.bal - input.amount == 0)
+ {
+ state.mut().generalAssetBalances.removeByKey(locals.key);
+ }
+ else
+ {
+ state.mut().generalAssetBalances.set(locals.key, locals.bal - input.amount);
+ }
+ output.returnCode = QTREAT_OK;
+ }
+
+ REGISTER_USER_FUNCTIONS_AND_PROCEDURES()
+ {
+ REGISTER_USER_FUNCTION(GetStakingInfo, 1);
+ REGISTER_USER_FUNCTION(GetPhaseInfo, 2);
+ REGISTER_USER_FUNCTION(GetFunds, 3);
+ REGISTER_USER_FUNCTION(GetRaffleInfo, 4);
+ REGISTER_USER_FUNCTION(GetExcludeAddresses, 5);
+ REGISTER_USER_FUNCTION(GetNftInfo, 6);
+ REGISTER_USER_FUNCTION(GetMinerInfo, 7);
+ REGISTER_USER_FUNCTION(GetAsicCatalogInfo, 8);
+
+ REGISTER_USER_PROCEDURE(DepositDividends, 1);
+ REGISTER_USER_PROCEDURE(DepositStakingFund, 2);
+ REGISTER_USER_PROCEDURE(RequestUnstake, 3);
+ REGISTER_USER_PROCEDURE(FinalizeUnstake, 4);
+ REGISTER_USER_PROCEDURE(ClaimQtreatBonus, 5);
+ REGISTER_USER_PROCEDURE(DepositQtreatTokens, 6);
+ REGISTER_USER_PROCEDURE(DepositGeneralAsset, 7);
+ REGISTER_USER_PROCEDURE(SetExcludeAddress, 8);
+ REGISTER_USER_PROCEDURE(RevokeGeneralAsset, 9);
+ REGISTER_USER_PROCEDURE(ReleaseManagedShares, 10);
+ REGISTER_USER_PROCEDURE(DepositMiningFund, 11);
+ REGISTER_USER_PROCEDURE(LoadAsicPart, 12);
+ REGISTER_USER_PROCEDURE(RegisterAsic, 13);
+ REGISTER_USER_PROCEDURE(UnregisterAsic, 14);
+ REGISTER_USER_PROCEDURE(DepositDripQdoge, 15);
+ REGISTER_USER_PROCEDURE(SetMiningRate, 16);
+ }
+
+ INITIALIZE()
+ {
+ state.mut().qtreatToken.issuer = ID(
+ _Q, _D, _O, _G, _E, _E, _E, _S,
+ _K, _Y, _P, _A, _I, _C, _E, _C,
+ _H, _E, _A, _H, _O, _X, _P, _U,
+ _L, _E, _O, _A, _D, _T, _K, _G,
+ _E, _J, _H, _A, _V, _Y, _P, _F,
+ _K, _H, _L, _E, _W, _G, _X, _X,
+ _Z, _Q, _U, _G, _I, _G, _M, _B
+ );
+ state.mut().qtreatToken.assetName = QTREAT_TOKEN_ASSETNAME;
+
+ state.mut().qdogeToken.issuer = state.get().qtreatToken.issuer;
+ state.mut().qdogeToken.assetName = QTREAT_QDOGE_ASSETNAME;
+
+ // Admin address: QTREATZZIVFYQAIBKCZPSHGLIRMALZKHEWAPFLFXJAMDAXMGTBKQVXHHDHUD
+ state.mut().adminAddress = ID(
+ _Q, _T, _R, _E, _A, _T, _Z, _Z,
+ _I, _V, _F, _Y, _Q, _A, _I, _B,
+ _K, _C, _Z, _P, _S, _H, _G, _L,
+ _I, _R, _M, _A, _L, _Z, _K, _H,
+ _E, _W, _A, _P, _F, _L, _F, _X,
+ _J, _A, _M, _D, _A, _X, _M, _G,
+ _T, _B, _K, _Q, _V, _X, _H, _H
+ );
+
+ state.mut().dividendNftIds.set(0, 4968);
+ state.mut().dividendNftIds.set(1, 4969);
+ state.mut().dividendNftIds.set(2, 4970);
+ state.mut().dividendNftIds.set(3, 4971);
+ state.mut().dividendNftIds.set(4, 4972);
+ state.mut().dividendNftIds.set(5, 4973);
+ state.mut().dividendNftIds.set(6, 4974);
+ state.mut().dividendNftIds.set(7, 4975);
+ state.mut().dividendNftIds.set(8, 4976);
+ state.mut().dividendNftIds.set(9, 4977);
+ state.mut().dividendNftIds.set(10, 4978);
+ state.mut().dividendNftIds.set(11, 4979);
+ state.mut().dividendNftIds.set(12, 4980);
+ state.mut().dividendNftIds.set(13, 4981);
+ state.mut().dividendNftIds.set(14, 4982);
+ state.mut().dividendNftIds.set(15, 4983);
+ state.mut().dividendNftIds.set(16, 4984);
+ state.mut().dividendNftIds.set(17, 4985);
+ state.mut().dividendNftIds.set(18, 4986);
+ state.mut().dividendNftIds.set(19, 4987);
+ state.mut().dividendNftIds.set(20, 4988);
+ state.mut().dividendNftIds.set(21, 4989);
+ state.mut().dividendNftIds.set(22, 4990);
+ state.mut().dividendNftIds.set(23, 4991);
+ state.mut().dividendNftIds.set(24, 4992);
+ state.mut().dividendNftIds.set(25, 4993);
+ state.mut().dividendNftIds.set(26, 4994);
+ state.mut().dividendNftIds.set(27, 4995);
+ state.mut().dividendNftIds.set(28, 4996);
+ state.mut().dividendNftIds.set(29, 4997);
+ state.mut().dividendNftIds.set(30, 4998);
+ state.mut().dividendNftIds.set(31, 4999);
+ state.mut().dividendNftIds.set(32, 5000);
+ state.mut().dividendNftIds.set(33, 5001);
+ state.mut().dividendNftIds.set(34, 5002);
+ state.mut().dividendNftIds.set(35, 5003);
+ state.mut().dividendNftIds.set(36, 5004);
+ state.mut().dividendNftIds.set(37, 5005);
+ state.mut().dividendNftIds.set(38, 5006);
+ state.mut().dividendNftIds.set(39, 5007);
+ state.mut().dividendNftIds.set(40, 5008);
+ state.mut().dividendNftIds.set(41, 5009);
+ state.mut().dividendNftIds.set(42, 5010);
+ state.mut().dividendNftIds.set(43, 5011);
+ state.mut().dividendNftIds.set(44, 5012);
+ state.mut().dividendNftIds.set(45, 5013);
+ state.mut().dividendNftIds.set(46, 5014);
+ state.mut().dividendNftIds.set(47, 5015);
+ state.mut().dividendNftIds.set(48, 5016);
+ state.mut().dividendNftIds.set(49, 5017);
+ state.mut().dividendNftIds.set(50, 5018);
+ state.mut().dividendNftIds.set(51, 5019);
+ state.mut().dividendNftIds.set(52, 5020);
+ state.mut().dividendNftIds.set(53, 5021);
+ state.mut().dividendNftIds.set(54, 5022);
+ state.mut().dividendNftIds.set(55, 5023);
+ state.mut().dividendNftIds.set(56, 5024);
+ state.mut().dividendNftIds.set(57, 5025);
+ state.mut().dividendNftIds.set(58, 5026);
+ state.mut().dividendNftIds.set(59, 5027);
+ state.mut().dividendNftIds.set(60, 5028);
+ state.mut().dividendNftIds.set(61, 5029);
+ state.mut().dividendNftIds.set(62, 5030);
+ state.mut().dividendNftIds.set(63, 5031);
+ state.mut().dividendNftIds.set(64, 5032);
+ state.mut().dividendNftIds.set(65, 5033);
+ state.mut().dividendNftIds.set(66, 5034);
+ state.mut().dividendNftIds.set(67, 5035);
+ state.mut().dividendNftIds.set(68, 5036);
+ state.mut().dividendNftIds.set(69, 5037);
+ state.mut().dividendNftIds.set(70, 5038);
+ state.mut().dividendNftIds.set(71, 5039);
+ state.mut().dividendNftIds.set(72, 5040);
+ state.mut().dividendNftIds.set(73, 5041);
+ state.mut().dividendNftIds.set(74, 5042);
+ state.mut().dividendNftIds.set(75, 5043);
+ state.mut().dividendNftIds.set(76, 5044);
+ state.mut().dividendNftIds.set(77, 5045);
+ state.mut().dividendNftIds.set(78, 5046);
+ state.mut().dividendNftIds.set(79, 5047);
+ state.mut().dividendNftIds.set(80, 5048);
+ state.mut().dividendNftIds.set(81, 5049);
+ state.mut().dividendNftIds.set(82, 5050);
+ state.mut().dividendNftIds.set(83, 5051);
+ state.mut().dividendNftIds.set(84, 5052);
+ state.mut().dividendNftIds.set(85, 5053);
+ state.mut().dividendNftIds.set(86, 5054);
+ state.mut().dividendNftIds.set(87, 5055);
+ state.mut().dividendNftIds.set(88, 5056);
+ state.mut().dividendNftIds.set(89, 5057);
+ state.mut().dividendNftIds.set(90, 5058);
+ state.mut().dividendNftIds.set(91, 5059);
+ state.mut().dividendNftIds.set(92, 5060);
+ state.mut().dividendNftIds.set(93, 5061);
+ state.mut().dividendNftIds.set(94, 5062);
+ state.mut().dividendNftIds.set(95, 5063);
+ state.mut().dividendNftIds.set(96, 5064);
+ state.mut().dividendNftIds.set(97, 5065);
+ state.mut().dividendNftIds.set(98, 5066);
+ state.mut().dividendNftIds.set(99, 5067);
+ state.mut().dividendNftIds.set(100, 5068);
+ state.mut().dividendNftIds.set(101, 5069);
+ state.mut().dividendNftIds.set(102, 5070);
+ state.mut().dividendNftIds.set(103, 5071);
+ state.mut().dividendNftIds.set(104, 5072);
+ state.mut().dividendNftIds.set(105, 5073);
+ state.mut().dividendNftIds.set(106, 5074);
+ state.mut().dividendNftIds.set(107, 5075);
+ state.mut().dividendNftIds.set(108, 5076);
+ state.mut().dividendNftIds.set(109, 5077);
+ state.mut().dividendNftIds.set(110, 5078);
+ state.mut().dividendNftIds.set(111, 5079);
+ state.mut().dividendNftIds.set(112, 5080);
+ state.mut().dividendNftIds.set(113, 5081);
+ state.mut().dividendNftIds.set(114, 5082);
+ state.mut().dividendNftIds.set(115, 5083);
+ state.mut().dividendNftIds.set(116, 5084);
+ state.mut().dividendNftIds.set(117, 5085);
+ state.mut().dividendNftIds.set(118, 5086);
+ state.mut().dividendNftIds.set(119, 5087);
+ state.mut().dividendNftIds.set(120, 5088);
+ state.mut().dividendNftIds.set(121, 5089);
+ state.mut().dividendNftIds.set(122, 5090);
+ state.mut().dividendNftIds.set(123, 5091);
+ state.mut().dividendNftIds.set(124, 5092);
+ state.mut().dividendNftIds.set(125, 5093);
+ state.mut().dividendNftIds.set(126, 5094);
+ state.mut().dividendNftIds.set(127, 5095);
+ state.mut().dividendNftIds.set(128, 5096);
+ state.mut().dividendNftIds.set(129, 5097);
+ state.mut().dividendNftIds.set(130, 5098);
+ state.mut().dividendNftIds.set(131, 5099);
+ state.mut().dividendNftIds.set(132, 5100);
+ state.mut().dividendNftIds.set(133, 5101);
+ state.mut().dividendNftIds.set(134, 5102);
+ state.mut().dividendNftIds.set(135, 5103);
+ state.mut().dividendNftIds.set(136, 5104);
+ state.mut().dividendNftIds.set(137, 5105);
+ state.mut().dividendNftIds.set(138, 5106);
+ state.mut().dividendNftIds.set(139, 5107);
+ state.mut().dividendNftIds.set(140, 5108);
+ state.mut().dividendNftIds.set(141, 5109);
+ state.mut().dividendNftIds.set(142, 5110);
+ state.mut().dividendNftIds.set(143, 5111);
+ state.mut().dividendNftIds.set(144, 5112);
+ state.mut().dividendNftIds.set(145, 5113);
+ state.mut().dividendNftIds.set(146, 5114);
+ state.mut().dividendNftIds.set(147, 5117);
+ state.mut().dividendNftIds.set(148, 5118);
+ state.mut().dividendNftIds.set(149, 5119);
+ state.mut().dividendNftIds.set(150, 5120);
+ state.mut().dividendNftIds.set(151, 5121);
+ state.mut().dividendNftIds.set(152, 5122);
+ state.mut().dividendNftIds.set(153, 5123);
+ state.mut().dividendNftIds.set(154, 5124);
+ state.mut().dividendNftIds.set(155, 5125);
+ state.mut().dividendNftIds.set(156, 5126);
+ state.mut().dividendNftIds.set(157, 5127);
+ state.mut().dividendNftIds.set(158, 5128);
+ state.mut().dividendNftIds.set(159, 5129);
+ state.mut().dividendNftIds.set(160, 5130);
+ state.mut().dividendNftIds.set(161, 5131);
+ state.mut().dividendNftIds.set(162, 5132);
+ state.mut().dividendNftIds.set(163, 5133);
+ state.mut().dividendNftIds.set(164, 5134);
+ state.mut().dividendNftIds.set(165, 5135);
+ state.mut().dividendNftIds.set(166, 5136);
+ state.mut().dividendNftIds.set(167, 5137);
+ state.mut().dividendNftIds.set(168, 5138);
+ state.mut().dividendNftIds.set(169, 5139);
+ state.mut().dividendNftIds.set(170, 5140);
+ state.mut().dividendNftIds.set(171, 5141);
+ state.mut().dividendNftIds.set(172, 5142);
+ state.mut().dividendNftIds.set(173, 5143);
+ state.mut().dividendNftIds.set(174, 5144);
+ state.mut().dividendNftIds.set(175, 5145);
+ state.mut().dividendNftIds.set(176, 5146);
+ state.mut().dividendNftIds.set(177, 5147);
+ state.mut().dividendNftIds.set(178, 5148);
+ state.mut().dividendNftIds.set(179, 5149);
+ state.mut().dividendNftIds.set(180, 5150);
+ state.mut().dividendNftIds.set(181, 5151);
+ state.mut().dividendNftIds.set(182, 5152);
+ state.mut().dividendNftIds.set(183, 5153);
+ state.mut().dividendNftIds.set(184, 5154);
+ state.mut().dividendNftIds.set(185, 5155);
+ state.mut().dividendNftIds.set(186, 5156);
+ state.mut().dividendNftIds.set(187, 5157);
+ state.mut().dividendNftIds.set(188, 5158);
+ state.mut().dividendNftIds.set(189, 5159);
+ state.mut().dividendNftIds.set(190, 5160);
+ state.mut().dividendNftIds.set(191, 5161);
+ state.mut().dividendNftIds.set(192, 5162);
+ state.mut().dividendNftIds.set(193, 5163);
+ state.mut().dividendNftIds.set(194, 5164);
+ state.mut().dividendNftIds.set(195, 5170);
+ state.mut().dividendNftIds.set(196, 5172);
+ state.mut().dividendNftIds.set(197, 5173);
+ state.mut().dividendNftIds.set(198, 5174);
+ state.mut().dividendNftIds.set(199, 5175);
+ state.mut().dividendNftIdCount = 200;
+
+ state.mut().miningRewardRate = QTREAT_MINING_REWARD_DEFAULT;
+
+ state.mut().stakingStartEpoch = 0;
+ }
+
+ struct POST_INCOMING_TRANSFER_locals { uint64 prev; };
+ POST_INCOMING_TRANSFER_WITH_LOCALS()
+ {
+ if (input.type == TransferType::qpiDistributeDividends)
+ {
+ state.mut().dividendFund = sadd(state.get().dividendFund, (uint64)input.amount);
+ locals.prev = 0;
+ state.get().scDividendTracker.get(input.sourceId, locals.prev);
+ state.mut().scDividendTracker.set(input.sourceId, sadd(locals.prev, (uint64)input.amount));
+ return;
+ }
+ if (input.type == TransferType::qpiTransfer
+ && input.sourceId == id(RANDOM_CONTRACT_INDEX, 0, 0, 0))
+ {
+ state.mut().stakingFund = sadd(state.get().stakingFund, (uint64)input.amount);
+ return;
+ }
+ if (input.type == TransferType::standardTransaction
+ || input.type == TransferType::qpiTransfer
+ || input.type == TransferType::revenueDonation)
+ {
+ state.mut().dividendFund = sadd(state.get().dividendFund, (uint64)input.amount);
+ }
+ }
+
+ struct BEGIN_EPOCH_locals { AssetPossessionIterator it; uint64 bal; id h; uint64 existing; sint64 exIdx; uint64 excluded; };
+ BEGIN_EPOCH_WITH_LOCALS()
+ {
+ if (state.get().stakingStartEpoch == 0)
+ {
+ state.mut().stakingStartEpoch = qpi.epoch();
+ }
+
+ state.mut().beginBalances.reset();
+ state.mut().endBalances.reset();
+ state.mut().totalHoldersSnapshot = 0;
+ if (state.get().qtreatToken.issuer != NULL_ID)
+ {
+ for (locals.it.begin(state.get().qtreatToken); !locals.it.reachedEnd(); locals.it.next())
+ {
+ if (locals.it.possessor() == SELF) continue;
+ locals.bal = locals.it.numberOfPossessedShares();
+ if (locals.bal < QTREAT_MIN_ELIGIBLE_BAL) continue;
+ locals.h = locals.it.possessor();
+ locals.excluded = 0;
+ for (locals.exIdx = 0; locals.exIdx < (sint64)QTREAT_MAX_EXCLUDE_ADDRESSES; locals.exIdx++)
+ {
+ if (state.get().excludeAddresses.get(locals.exIdx) != NULL_ID
+ && locals.h == state.get().excludeAddresses.get(locals.exIdx))
+ {
+ locals.excluded = 1;
+ }
+ }
+ if (locals.excluded != 0) continue;
+ locals.existing = 0;
+ state.get().beginBalances.get(locals.h, locals.existing);
+ if (state.mut().beginBalances.set(locals.h, sadd(locals.existing, locals.bal)) != NULL_INDEX)
+ {
+ state.mut().totalHoldersSnapshot = sadd(state.get().totalHoldersSnapshot, locals.bal);
+ }
+ }
+ }
+ }
+
+ struct END_EPOCH_locals
+ {
+ AssetPossessionIterator it; uint64 bal; id h; uint64 existing;
+ sint64 sidx; StakerInfo info; uint64 rewardBudget; uint64 reward; uint64 q; uint64 rem;
+ uint64 phaseActive; uint64 elapsed;
+ RANDOM::BuyEntropy_input entropyIn;
+ RANDOM::BuyEntropy_output entropyOut;
+ bit_4096 zeroEntropy;
+ RaffleSeed seed;
+ m256i raffleDigest;
+ uint64 eligibleCount; uint64 winnerIdx; uint64 n;
+ sint64 relResult;
+ sint64 walletBal; uint64 holdings; uint64 totalWeight; uint64 weight;
+ uint64 amount; sint64 idx; uint64 beginBal; uint64 endBal; uint64 eligible;
+ uint64 nftCnt; uint64 totalEligible; sint64 exIdx; uint64 isExcluded;
+ uint64 shareholderShare; uint64 perShare; uint64 paidShareholders;
+ uint64 reservedDividend; uint64 distributedDividend;
+ uint64 dripRate; uint64 dripDue; sint64 dripIdx; uint64 dripPacked; uint64 dripRarity;
+ sint64 dripXfer; sint64 dripManaged; uint64 dripAccounted; StakerInfo dripStakerInfo;
+ AsicRig rig; uint64 miningBudget; uint64 minerBudget; uint64 miningDividendCut;
+ uint64 minerReward; uint64 stillOwned; uint32 rigPartId; sint64 pIdx; uint64 verifyOk;
+ QBAY::getInfoOfNFTById_input qbayIn; QBAY::getInfoOfNFTById_output qbayOut;
+ Entity ent; uint64 contractBalance;
+ };
+ END_EPOCH_WITH_LOCALS()
+ {
+ state.mut().beginBalances.cleanupIfNeeded();
+ state.mut().endBalances.cleanupIfNeeded();
+ state.mut().stakers.cleanupIfNeeded();
+ state.mut().generalAssetBalances.cleanupIfNeeded();
+ state.mut().scDividendTracker.cleanupIfNeeded();
+ state.mut().nftCounts.cleanupIfNeeded();
+ state.mut().asicUsedParts.cleanupIfNeeded();
+
+ for (locals.sidx = state.get().stakers.nextElementIndex(NULL_INDEX);
+ locals.sidx != NULL_INDEX;
+ locals.sidx = state.get().stakers.nextElementIndex(locals.sidx))
+ {
+ locals.info = state.get().stakers.value(locals.sidx);
+ if (locals.info.unstakeAmount == 0) continue;
+ if (qpi.epoch() < locals.info.unstakeEpoch + QTREAT_UNSTAKE_DELAY_EPOCHS) continue;
+
+ locals.h = state.get().stakers.key(locals.sidx);
+ locals.relResult = qpi.releaseShares(state.get().qdogeToken, locals.h, locals.h,
+ (sint64)locals.info.unstakeAmount, QTREAT_QX_CONTRACT_INDEX, QTREAT_QX_CONTRACT_INDEX,
+ QTREAT_QX_TRANSFER_FEE);
+ if (locals.relResult < 0) continue;
+
+ locals.info.unstakeAmount = 0;
+ locals.info.unstakeEpoch = 0;
+ if (locals.info.staked == 0 && locals.info.pendingBonus == 0
+ && locals.info.bonusAwarded == 0)
+ {
+ state.mut().stakers.removeByKey(locals.h);
+ }
+ else
+ {
+ state.mut().stakers.set(locals.h, locals.info);
+ }
+ }
+
+ qpi.getEntity(SELF, locals.ent);
+ locals.contractBalance = locals.ent.incomingAmount - locals.ent.outgoingAmount;
+
+ locals.phaseActive = 0;
+ if (state.get().stakingStartEpoch != 0)
+ {
+ locals.elapsed = qpi.epoch() - state.get().stakingStartEpoch;
+ if (locals.elapsed < QTREAT_TOTAL_REWARD_EPOCHS) locals.phaseActive = 1;
+ }
+
+ if (locals.phaseActive == 1 && state.get().totalStaked > 0)
+ {
+ locals.rewardBudget = QTREAT_STAKER_REWARD_PER_EPOCH;
+ if (locals.rewardBudget > state.get().stakingFund) locals.rewardBudget = state.get().stakingFund;
+
+ locals.totalWeight = 0;
+ for (locals.sidx = state.get().stakers.nextElementIndex(NULL_INDEX);
+ locals.sidx != NULL_INDEX;
+ locals.sidx = state.get().stakers.nextElementIndex(locals.sidx))
+ {
+ locals.h = state.get().stakers.key(locals.sidx);
+ locals.info = state.get().stakers.value(locals.sidx);
+ if (locals.info.staked == 0)
+ {
+ if (locals.info.bonusEpochs != 0 || locals.info.growthStreak != 0
+ || locals.info.lastStaked != 0)
+ {
+ locals.info.bonusEpochs = 0;
+ locals.info.growthStreak = 0;
+ locals.info.lastStaked = 0;
+ state.mut().stakers.set(locals.h, locals.info);
+ }
+ continue;
+ }
+
+ locals.walletBal = qpi.numberOfPossessedShares(
+ state.get().qdogeToken.assetName, state.get().qdogeToken.issuer,
+ locals.h, locals.h, QTREAT_QX_CONTRACT_INDEX, QTREAT_QX_CONTRACT_INDEX);
+ if (locals.walletBal < 0) locals.walletBal = 0;
+ locals.holdings = sadd(sadd(locals.info.staked, locals.info.unstakeAmount), (uint64)locals.walletBal);
+ if (locals.elapsed < QTREAT_PROGRESSIVE_START_DELAY_EPOCHS)
+ {
+ locals.info.growthStreak = 0;
+ if (locals.holdings > locals.info.hwmHoldings)
+ {
+ locals.info.hwmHoldings = locals.holdings;
+ }
+ }
+ else if (locals.info.staked > locals.info.lastStaked
+ && locals.info.staked - locals.info.lastStaked >= QTREAT_PROGRESSIVE_MIN_STEP
+ && locals.holdings > locals.info.hwmHoldings)
+ {
+ if (locals.info.growthStreak < QTREAT_PROGRESSIVE_MAX_STREAK)
+ {
+ locals.info.growthStreak += 1;
+ }
+ locals.info.hwmHoldings = locals.holdings;
+ }
+ else
+ {
+ locals.info.growthStreak = 0;
+ }
+ locals.info.lastStaked = locals.info.staked;
+
+ if (locals.info.staked >= QTREAT_BONUS_THRESHOLD)
+ {
+ if (locals.info.bonusAwarded < QTREAT_BONUS_MAX_PER_WALLET)
+ {
+ locals.info.bonusEpochs += 1;
+ while (locals.info.bonusEpochs >= QTREAT_BONUS_INTERVAL_EPOCHS
+ && locals.info.bonusAwarded < QTREAT_BONUS_MAX_PER_WALLET)
+ {
+ locals.info.bonusEpochs -= QTREAT_BONUS_INTERVAL_EPOCHS;
+ locals.info.bonusAwarded += 1;
+ locals.info.pendingBonus += 1;
+ }
+ }
+ }
+ else
+ {
+ locals.info.bonusEpochs = 0;
+ }
+
+ state.mut().stakers.set(locals.h, locals.info);
+ locals.weight = ((uint128)locals.info.staked
+ * (uint128)(1000 + locals.info.growthStreak * QTREAT_PROGRESSIVE_BONUS_PERMILLE)).low;
+ locals.totalWeight = sadd(locals.totalWeight, locals.weight);
+ }
+
+ if (locals.rewardBudget > 0 && locals.totalWeight > 0)
+ {
+ for (locals.sidx = state.get().stakers.nextElementIndex(NULL_INDEX);
+ locals.sidx != NULL_INDEX;
+ locals.sidx = state.get().stakers.nextElementIndex(locals.sidx))
+ {
+ locals.h = state.get().stakers.key(locals.sidx);
+ locals.info = state.get().stakers.value(locals.sidx);
+ if (locals.info.staked == 0) continue;
+
+ locals.weight = ((uint128)locals.info.staked
+ * (uint128)(1000 + locals.info.growthStreak * QTREAT_PROGRESSIVE_BONUS_PERMILLE)).low;
+ locals.reward = div((uint128)locals.rewardBudget * (uint128)locals.weight,
+ (uint128)locals.totalWeight).low;
+ if (locals.reward > locals.contractBalance) locals.reward = locals.contractBalance;
+ if (locals.reward == 0) continue;
+
+ qpi.transfer(locals.h, locals.reward);
+ locals.contractBalance -= locals.reward;
+ state.mut().stakingFund = state.get().stakingFund - locals.reward;
+ state.mut().totalStakingRewardsDistributed =
+ sadd(state.get().totalStakingRewardsDistributed, locals.reward);
+ }
+ }
+ }
+
+ if (state.get().stakingStartEpoch != 0
+ && qpi.epoch() - state.get().stakingStartEpoch < QTREAT_RAFFLE_EPOCHS
+ && state.get().qtreatBonusPool > 0
+ && state.get().stakingFund >= QTREAT_RAFFLE_ENTROPY_FEE)
+ {
+ locals.eligibleCount = 0;
+ for (locals.sidx = state.get().stakers.nextElementIndex(NULL_INDEX);
+ locals.sidx != NULL_INDEX;
+ locals.sidx = state.get().stakers.nextElementIndex(locals.sidx))
+ {
+ locals.info = state.get().stakers.value(locals.sidx);
+ if (locals.info.staked > 0 && locals.info.unstakeAmount == 0)
+ {
+ locals.eligibleCount++;
+ }
+ }
+
+ if (locals.eligibleCount > 0)
+ {
+ state.mut().stakingFund = state.get().stakingFund - QTREAT_RAFFLE_ENTROPY_FEE;
+ locals.entropyIn.collateralTier = QTREAT_RAFFLE_COLLATERAL_TIER;
+ locals.entropyIn.numberOfBits = QTREAT_RAFFLE_ENTROPY_BITS;
+ locals.entropyIn.trustee = id::zero();
+ INVOKE_OTHER_CONTRACT_PROCEDURE(RANDOM, BuyEntropy, locals.entropyIn, locals.entropyOut, (sint64)QTREAT_RAFFLE_ENTROPY_FEE);
+
+ if (interContractCallError != NoCallError)
+ {
+ state.mut().stakingFund = sadd(state.get().stakingFund, QTREAT_RAFFLE_ENTROPY_FEE);
+ // Failed invoke leaves output stale; zero it.
+ locals.entropyOut.entropy = locals.zeroEntropy;
+ }
+
+ // Fallback seed is public and grindable without RANDOM.
+ locals.seed.entropy = locals.entropyOut.entropy;
+ locals.seed.prevSpectrumDigest = qpi.getPrevSpectrumDigest();
+ locals.seed.prevUniverseDigest = qpi.getPrevUniverseDigest();
+ locals.seed.prevComputerDigest = qpi.getPrevComputerDigest();
+ locals.seed.epoch = qpi.epoch();
+ locals.seed.tick = qpi.tick();
+ locals.seed.eligibleCount = locals.eligibleCount;
+ locals.raffleDigest = qpi.K12(locals.seed);
+ locals.winnerIdx = mod((uint64)locals.raffleDigest.u64._0, locals.eligibleCount);
+
+ locals.n = 0;
+ for (locals.sidx = state.get().stakers.nextElementIndex(NULL_INDEX);
+ locals.sidx != NULL_INDEX;
+ locals.sidx = state.get().stakers.nextElementIndex(locals.sidx))
+ {
+ locals.info = state.get().stakers.value(locals.sidx);
+ if (locals.info.staked == 0 || locals.info.unstakeAmount > 0) continue;
+ if (locals.n == locals.winnerIdx)
+ {
+ locals.h = state.get().stakers.key(locals.sidx);
+ locals.info.pendingBonus += 1;
+ state.mut().stakers.set(locals.h, locals.info);
+ state.mut().totalRaffleAwarded = state.get().totalRaffleAwarded + 1;
+ state.mut().lastRaffleWinner = locals.h;
+ state.mut().lastRaffleEpoch = qpi.epoch();
+ break;
+ }
+ locals.n++;
+ }
+ }
+ }
+
+ for (locals.sidx = state.get().stakers.nextElementIndex(NULL_INDEX);
+ locals.sidx != NULL_INDEX;
+ locals.sidx = state.get().stakers.nextElementIndex(locals.sidx))
+ {
+ locals.info = state.get().stakers.value(locals.sidx);
+ if (locals.info.pendingBonus == 0) continue;
+ if (state.get().qtreatBonusPool < locals.info.pendingBonus) continue;
+ if (state.get().stakingFund < (uint64)QTREAT_QX_TRANSFER_FEE) continue;
+
+ locals.h = state.get().stakers.key(locals.sidx);
+ if (qpi.transferShareOwnershipAndPossession(
+ state.get().qtreatToken.assetName, state.get().qtreatToken.issuer,
+ SELF, SELF, (sint64)locals.info.pendingBonus, locals.h) < 0)
+ {
+ continue;
+ }
+ state.mut().qtreatBonusPool = state.get().qtreatBonusPool - locals.info.pendingBonus;
+ state.mut().totalBonusDelivered = sadd(state.get().totalBonusDelivered, locals.info.pendingBonus);
+ state.mut().stakingFund = state.get().stakingFund - (uint64)QTREAT_QX_TRANSFER_FEE;
+ qpi.releaseShares(state.get().qtreatToken, locals.h, locals.h,
+ (sint64)locals.info.pendingBonus, QTREAT_QX_CONTRACT_INDEX, QTREAT_QX_CONTRACT_INDEX,
+ QTREAT_QX_TRANSFER_FEE);
+
+ locals.info.pendingBonus = 0;
+ state.mut().stakers.set(locals.h, locals.info);
+ }
+
+ // Mining budget envelope for this epoch, computed once. The dividend half is cut
+ // unconditionally below (it isn't tied to any individual rig's ownership); the miner
+ // half is only ever paid to a rig in the same pass that freshly re-verifies it (see
+ // the merged loop below) so a sold/transferred part can never keep drawing rewards
+ // for its old owner.
+ locals.minerBudget = 0;
+ if (state.get().totalMiningWeight > 0 && state.get().miningFund > 0)
+ {
+ qpi.getEntity(SELF, locals.ent);
+ locals.contractBalance = locals.ent.incomingAmount - locals.ent.outgoingAmount;
+ locals.miningBudget = state.get().miningRewardRate;
+ if (locals.miningBudget > state.get().miningFund) locals.miningBudget = state.get().miningFund;
+ // Only ~1/QTREAT_ASIC_VERIFY_SPREAD_EPOCHS of rigs are due for (scaled-up) payment
+ // in any given epoch, so take this epoch's dividend cut from the same fair 1/N
+ // slice of the budget instead of the full envelope - otherwise the dividend cut
+ // would drain miningFund at full speed every epoch regardless of how many rigs
+ // are actually due, starving the miner side before rigs get their scaled turn
+ // (especially with few registered rigs, where most epochs have nobody due at all).
+ locals.miningBudget = div(locals.miningBudget, QTREAT_ASIC_VERIFY_SPREAD_EPOCHS);
+
+ // Split 50/50; odd QU goes to dividends.
+ locals.minerBudget = div(locals.miningBudget, 2ULL);
+ locals.miningDividendCut = locals.miningBudget - locals.minerBudget;
+
+ if (locals.miningDividendCut > 0)
+ {
+ state.mut().miningFund = state.get().miningFund - locals.miningDividendCut;
+ state.mut().dividendFund = sadd(state.get().dividendFund, locals.miningDividendCut);
+ }
+ }
+
+ for (locals.sidx = 0; locals.sidx < (sint64)state.get().asicRigHighWater; locals.sidx++)
+ {
+ locals.rig = state.get().asicRigs.get(locals.sidx);
+ if (locals.rig.active == 0) continue;
+ // Only re-verify (and potentially pay) a rotating slice of rigs this epoch (see
+ // QTREAT_ASIC_VERIFY_SPREAD_EPOCHS); un-checked rigs keep their current active
+ // state and simply wait for their next scheduled turn - no ownership-dependent
+ // payment happens for them outside their own verification epoch.
+ if (mod((uint64)locals.sidx + qpi.epoch(), QTREAT_ASIC_VERIFY_SPREAD_EPOCHS) != 0) continue;
+ locals.stillOwned = 1;
+ locals.verifyOk = 1;
+ for (locals.pIdx = 0; locals.pIdx < 4; locals.pIdx++)
+ {
+ if (locals.pIdx == 0) locals.rigPartId = locals.rig.partMotherboard;
+ if (locals.pIdx == 1) locals.rigPartId = locals.rig.partChip;
+ if (locals.pIdx == 2) locals.rigPartId = locals.rig.partPsu;
+ if (locals.pIdx == 3) locals.rigPartId = locals.rig.partFan;
+ locals.qbayIn.NFTId = locals.rigPartId;
+ CALL_OTHER_CONTRACT_FUNCTION(QBAY, getInfoOfNFTById, locals.qbayIn, locals.qbayOut);
+ if (interContractCallError != NoCallError) { locals.verifyOk = 0; break; }
+ if (locals.qbayOut.possessor != locals.rig.owner)
+ {
+ locals.stillOwned = 0;
+ }
+ }
+ if (locals.verifyOk == 0) continue; // inconclusive this epoch; try again next scheduled turn
+
+ if (locals.stillOwned == 0)
+ {
+ state.mut().asicUsedParts.removeByKey((uint64)locals.rig.partMotherboard);
+ state.mut().asicUsedParts.removeByKey((uint64)locals.rig.partChip);
+ state.mut().asicUsedParts.removeByKey((uint64)locals.rig.partPsu);
+ state.mut().asicUsedParts.removeByKey((uint64)locals.rig.partFan);
+ state.mut().totalAsicCount = state.get().totalAsicCount - 1;
+ state.mut().totalMiningWeight = state.get().totalMiningWeight - locals.rig.weight;
+ locals.rig.active = 0;
+ state.mut().asicRigs.set(locals.sidx, locals.rig);
+ continue;
+ }
+
+ // Freshly verified as still valid this epoch: pay its share now, scaled by the
+ // spread factor to compensate for only being eligible on 1-in-N epochs (so the
+ // rig's total earnings over a full verification cycle match what continuous
+ // per-epoch payment at the same weight ratio would have produced).
+ if (locals.minerBudget > 0 && locals.rig.weight > 0 && state.get().totalMiningWeight > 0)
+ {
+ locals.minerReward = div((uint128)locals.minerBudget * (uint128)locals.rig.weight
+ * (uint128)QTREAT_ASIC_VERIFY_SPREAD_EPOCHS, (uint128)state.get().totalMiningWeight).low;
+ if (locals.minerReward > 0)
+ {
+ if (locals.minerReward > locals.contractBalance) locals.minerReward = locals.contractBalance;
+ if (locals.minerReward > state.get().miningFund) locals.minerReward = state.get().miningFund;
+ if (locals.minerReward > 0)
+ {
+ qpi.transfer(locals.rig.owner, locals.minerReward);
+ locals.contractBalance -= locals.minerReward;
+ state.mut().miningFund = state.get().miningFund - locals.minerReward;
+ state.mut().totalMiningRewardsDistributed =
+ sadd(state.get().totalMiningRewardsDistributed, locals.minerReward);
+ }
+ }
+ }
+ }
+
+ if (state.get().dividendNftIdCount > 0)
+ {
+ // Only this epoch's rotating slice of ids (see QTREAT_NFT_SNAPSHOT_SPREAD_EPOCHS) is
+ // queried and counted - nftCounts is fully rebuilt from just that fresh slice every
+ // epoch (no carried-forward "last known possessor" state), so a holder only ever
+ // gets dividend credit for an id in the same epoch its current possessor is actually
+ // re-confirmed. A seller can't keep collecting after a sale, and the buyer starts
+ // getting credit as soon as their id's next scheduled check lands - never mid-cycle
+ // stale data either way. Each matched id counts QTREAT_NFT_SNAPSHOT_SPREAD_EPOCHS
+ // times (instead of once) to compensate for only ~1/N of the collection being checked
+ // in any given epoch, so the collection's aggregate share of the dividend pool matches
+ // what checking everyone every epoch would produce, in expectation over a full cycle.
+ state.mut().nftCounts.reset();
+ state.mut().totalNftCount = 0;
+ for (locals.sidx = 0; locals.sidx < (sint64)state.get().dividendNftIdCount; locals.sidx++)
+ {
+ if (mod((uint64)locals.sidx + qpi.epoch(), QTREAT_NFT_SNAPSHOT_SPREAD_EPOCHS) != 0) continue;
+
+ locals.qbayIn.NFTId = state.get().dividendNftIds.get(locals.sidx);
+ CALL_OTHER_CONTRACT_FUNCTION(QBAY, getInfoOfNFTById, locals.qbayIn, locals.qbayOut);
+ if (interContractCallError != NoCallError) continue;
+ if (locals.qbayOut.possessor == NULL_ID || locals.qbayOut.possessor == SELF) continue;
+ locals.isExcluded = 0;
+ for (locals.exIdx = 0; locals.exIdx < (sint64)QTREAT_MAX_EXCLUDE_ADDRESSES; locals.exIdx++)
+ {
+ if (state.get().excludeAddresses.get(locals.exIdx) != NULL_ID
+ && locals.qbayOut.possessor == state.get().excludeAddresses.get(locals.exIdx))
+ {
+ locals.isExcluded = 1;
+ }
+ }
+ if (locals.isExcluded != 0) continue;
+
+ locals.nftCnt = 0;
+ state.get().nftCounts.get(locals.qbayOut.possessor, locals.nftCnt);
+ state.mut().nftCounts.set(locals.qbayOut.possessor, locals.nftCnt + QTREAT_NFT_SNAPSHOT_SPREAD_EPOCHS);
+ state.mut().totalNftCount = state.get().totalNftCount + QTREAT_NFT_SNAPSHOT_SPREAD_EPOCHS;
+ }
+ }
+
+ if (state.get().asicCatalogLocked == 1
+ && state.get().dripStartEpoch != 0
+ && qpi.epoch() - state.get().dripStartEpoch < QTREAT_DRIP_EPOCHS
+ && state.get().dripQdogePool > 0)
+ {
+ state.mut().dripTally.reset();
+ for (locals.dripIdx = state.get().asicCatalog.nextElementIndex(NULL_INDEX);
+ locals.dripIdx != NULL_INDEX;
+ locals.dripIdx = state.get().asicCatalog.nextElementIndex(locals.dripIdx))
+ {
+ locals.qbayIn.NFTId = (uint32)state.get().asicCatalog.key(locals.dripIdx);
+ locals.dripPacked = state.get().asicCatalog.value(locals.dripIdx);
+ locals.dripRarity = mod(locals.dripPacked, 8ULL);
+ CALL_OTHER_CONTRACT_FUNCTION(QBAY, getInfoOfNFTById, locals.qbayIn, locals.qbayOut);
+ if (interContractCallError != NoCallError) continue;
+ if (locals.qbayOut.possessor == NULL_ID || locals.qbayOut.possessor == SELF) continue;
+ if (locals.qbayOut.possessor == state.get().adminAddress) continue;
+
+ locals.dripRate = QTREAT_DRIP_QDOGE_COMMON;
+ if (locals.dripRarity == 1) locals.dripRate = QTREAT_DRIP_QDOGE_UNCOMMON;
+ if (locals.dripRarity == 2) locals.dripRate = QTREAT_DRIP_QDOGE_RARE;
+ if (locals.dripRarity == 3) locals.dripRate = QTREAT_DRIP_QDOGE_EPIC;
+ if (locals.dripRarity == 4) locals.dripRate = QTREAT_DRIP_QDOGE_LEGENDARY;
+
+ locals.existing = 0;
+ state.get().dripTally.get(locals.qbayOut.possessor, locals.existing);
+ state.mut().dripTally.set(locals.qbayOut.possessor, locals.existing + locals.dripRate);
+ }
+
+ for (locals.dripIdx = state.get().dripTally.nextElementIndex(NULL_INDEX);
+ locals.dripIdx != NULL_INDEX;
+ locals.dripIdx = state.get().dripTally.nextElementIndex(locals.dripIdx))
+ {
+ locals.h = state.get().dripTally.key(locals.dripIdx);
+ locals.dripDue = state.get().dripTally.value(locals.dripIdx);
+ if (locals.dripDue == 0) continue;
+ if (state.get().dripQdogePool < locals.dripDue) continue;
+ if (state.get().miningFund < (uint64)QTREAT_QX_TRANSFER_FEE) break;
+
+ locals.dripXfer = qpi.transferShareOwnershipAndPossession(
+ state.get().qdogeToken.assetName, state.get().qdogeToken.issuer,
+ SELF, SELF, (sint64)locals.dripDue, locals.h);
+ if (locals.dripXfer < 0) continue;
+ state.mut().dripQdogePool = state.get().dripQdogePool - locals.dripDue;
+ state.mut().totalDripQdogeDistributed = sadd(state.get().totalDripQdogeDistributed, locals.dripDue);
+ state.mut().miningFund = state.get().miningFund - (uint64)QTREAT_QX_TRANSFER_FEE;
+ locals.dripManaged = qpi.numberOfPossessedShares(
+ state.get().qdogeToken.assetName, state.get().qdogeToken.issuer,
+ locals.h, locals.h, SELF_INDEX, SELF_INDEX);
+ locals.dripStakerInfo.staked = 0; locals.dripStakerInfo.unstakeAmount = 0;
+ state.get().stakers.get(locals.h, locals.dripStakerInfo);
+ locals.dripAccounted = locals.dripStakerInfo.staked + locals.dripStakerInfo.unstakeAmount;
+ if (locals.dripManaged > (sint64)locals.dripAccounted)
+ {
+ qpi.releaseShares(state.get().qdogeToken, locals.h, locals.h,
+ locals.dripManaged - (sint64)locals.dripAccounted,
+ QTREAT_QX_CONTRACT_INDEX, QTREAT_QX_CONTRACT_INDEX,
+ QTREAT_QX_TRANSFER_FEE);
+ }
+ }
+ }
+
+ if (state.get().qtreatToken.issuer != NULL_ID)
+ {
+ for (locals.it.begin(state.get().qtreatToken); !locals.it.reachedEnd(); locals.it.next())
+ {
+ if (locals.it.possessor() == SELF) continue;
+ locals.bal = locals.it.numberOfPossessedShares();
+ if (locals.bal == 0) continue;
+ locals.h = locals.it.possessor();
+ locals.existing = 0;
+ state.get().endBalances.get(locals.h, locals.existing);
+ state.mut().endBalances.set(locals.h, sadd(locals.existing, locals.bal));
+ }
+ }
+
+ if (state.get().dividendFund == 0) return;
+ qpi.getEntity(SELF, locals.ent);
+ locals.contractBalance = locals.ent.incomingAmount - locals.ent.outgoingAmount;
+ locals.amount = state.get().dividendFund;
+ if (locals.amount > locals.contractBalance) locals.amount = locals.contractBalance;
+ locals.totalEligible = sadd(state.get().totalHoldersSnapshot, state.get().totalNftCount);
+ if (locals.amount == 0 || locals.totalEligible == 0) return;
+
+ locals.reservedDividend = locals.amount;
+ locals.distributedDividend = 0;
+ state.mut().dividendFund = state.get().dividendFund - locals.reservedDividend;
+
+ locals.shareholderShare = div((uint128)locals.amount * (uint128)QTREAT_DIVIDEND_SHAREHOLDER_PERMILLE, (uint128)1000ULL).low;
+ locals.perShare = div(locals.shareholderShare, (uint64)NUMBER_OF_COMPUTORS);
+ if (locals.perShare > 0 && qpi.distributeDividends((sint64)locals.perShare))
+ {
+ locals.paidShareholders = locals.perShare * (uint64)NUMBER_OF_COMPUTORS;
+ locals.amount -= locals.paidShareholders;
+ locals.distributedDividend = locals.paidShareholders;
+ locals.contractBalance = (locals.contractBalance > locals.paidShareholders)
+ ? locals.contractBalance - locals.paidShareholders : 0;
+ state.mut().totalShareholderDividends = sadd(state.get().totalShareholderDividends, locals.paidShareholders);
+ }
+
+ for (locals.idx = state.get().beginBalances.nextElementIndex(NULL_INDEX);
+ locals.idx != NULL_INDEX;
+ locals.idx = state.get().beginBalances.nextElementIndex(locals.idx))
+ {
+ locals.h = state.get().beginBalances.key(locals.idx);
+ locals.beginBal = state.get().beginBalances.value(locals.idx);
+ locals.endBal = 0;
+ state.get().endBalances.get(locals.h, locals.endBal);
+ locals.eligible = (locals.endBal < locals.beginBal) ? locals.endBal : locals.beginBal;
+ locals.nftCnt = 0;
+ state.get().nftCounts.get(locals.h, locals.nftCnt);
+ locals.eligible = sadd(locals.eligible, locals.nftCnt);
+ if (locals.eligible == 0) continue;
+
+ locals.q = div(locals.amount, locals.totalEligible);
+ locals.rem = mod(locals.amount, locals.totalEligible);
+ locals.reward = locals.q * locals.eligible
+ + div((uint128)locals.rem * (uint128)locals.eligible, (uint128)locals.totalEligible).low;
+ if (locals.reward == 0) continue;
+ if (locals.reward > locals.contractBalance) locals.reward = locals.contractBalance;
+
+ qpi.transfer(locals.h, locals.reward);
+ locals.contractBalance -= locals.reward;
+ locals.distributedDividend = sadd(locals.distributedDividend, locals.reward);
+ if (locals.contractBalance == 0) break;
+ }
+
+ for (locals.idx = state.get().nftCounts.nextElementIndex(NULL_INDEX);
+ locals.idx != NULL_INDEX;
+ locals.idx = state.get().nftCounts.nextElementIndex(locals.idx))
+ {
+ locals.h = state.get().nftCounts.key(locals.idx);
+ if (state.get().beginBalances.contains(locals.h)) continue;
+ locals.isExcluded = 0;
+ for (locals.exIdx = 0; locals.exIdx < (sint64)QTREAT_MAX_EXCLUDE_ADDRESSES; locals.exIdx++)
+ {
+ if (state.get().excludeAddresses.get(locals.exIdx) != NULL_ID
+ && locals.h == state.get().excludeAddresses.get(locals.exIdx))
+ {
+ locals.isExcluded = 1;
+ }
+ }
+ if (locals.isExcluded != 0) continue;
+ locals.eligible = state.get().nftCounts.value(locals.idx);
+ if (locals.eligible == 0) continue;
+
+ locals.q = div(locals.amount, locals.totalEligible);
+ locals.rem = mod(locals.amount, locals.totalEligible);
+ locals.reward = locals.q * locals.eligible
+ + div((uint128)locals.rem * (uint128)locals.eligible, (uint128)locals.totalEligible).low;
+ if (locals.reward == 0) continue;
+ if (locals.reward > locals.contractBalance) locals.reward = locals.contractBalance;
+
+ qpi.transfer(locals.h, locals.reward);
+ locals.contractBalance -= locals.reward;
+ locals.distributedDividend = sadd(locals.distributedDividend, locals.reward);
+ if (locals.contractBalance == 0) break;
+ }
+
+ if (locals.reservedDividend > locals.distributedDividend)
+ {
+ state.mut().dividendFund = sadd(state.get().dividendFund,
+ locals.reservedDividend - locals.distributedDividend);
+ }
+ state.mut().totalDividendsDistributed =
+ sadd(state.get().totalDividendsDistributed, locals.distributedDividend);
+ }
+
+ struct PRE_ACQUIRE_SHARES_locals { StakerInfo info; };
+ PRE_ACQUIRE_SHARES_WITH_LOCALS()
+ {
+ if (input.otherContractIndex != QTREAT_QX_CONTRACT_INDEX) return;
+
+ if (input.asset.assetName == state.get().qdogeToken.assetName
+ && input.asset.issuer == state.get().qdogeToken.issuer)
+ {
+ if (input.owner == state.get().adminAddress)
+ {
+ output.allowTransfer = true;
+ output.requestedFee = 0;
+ return;
+ }
+ if (input.numberOfShares <= 0) return;
+ locals.info.staked = 0; locals.info.unstakeAmount = 0; locals.info.unstakeEpoch = 0;
+ locals.info.bonusEpochs = 0; locals.info.bonusAwarded = 0; locals.info.pendingBonus = 0;
+ state.get().stakers.get(input.owner, locals.info);
+ if (state.get().stakingStartEpoch != 0
+ && qpi.epoch() - state.get().stakingStartEpoch >= QTREAT_TOTAL_REWARD_EPOCHS) return;
+ if (locals.info.unstakeAmount > 0) return;
+ if (locals.info.staked + (uint64)input.numberOfShares < QTREAT_MIN_STAKE) return;
+ if (!state.get().stakers.contains(input.owner)
+ && state.get().stakers.population() >= QTREAT_MAX_STAKERS) return;
+ output.allowTransfer = true;
+ output.requestedFee = 0;
+ return;
+ }
+
+ output.allowTransfer = true;
+ output.requestedFee = 0;
+ }
+
+ struct POST_ACQUIRE_SHARES_locals { StakerInfo info; uint64 existed; sint64 wallet; };
+ POST_ACQUIRE_SHARES_WITH_LOCALS()
+ {
+ if (input.otherContractIndex != QTREAT_QX_CONTRACT_INDEX) return;
+ if (input.asset.assetName != state.get().qdogeToken.assetName
+ || input.asset.issuer != state.get().qdogeToken.issuer) return;
+ if (input.owner == state.get().adminAddress) return;
+ if (input.numberOfShares <= 0) return;
+
+ locals.info.staked = 0; locals.info.unstakeAmount = 0; locals.info.unstakeEpoch = 0;
+ locals.info.bonusEpochs = 0; locals.info.bonusAwarded = 0; locals.info.pendingBonus = 0;
+ locals.info.hwmHoldings = 0; locals.info.lastStaked = 0; locals.info.growthStreak = 0;
+ locals.existed = state.get().stakers.get(input.owner, locals.info) ? 1 : 0;
+ locals.info.staked = sadd(locals.info.staked, (uint64)input.numberOfShares);
+ if (locals.existed == 0)
+ {
+ locals.wallet = qpi.numberOfPossessedShares(
+ state.get().qdogeToken.assetName, state.get().qdogeToken.issuer,
+ input.owner, input.owner, QTREAT_QX_CONTRACT_INDEX, QTREAT_QX_CONTRACT_INDEX);
+ if (locals.wallet < 0) locals.wallet = 0;
+ locals.info.hwmHoldings = sadd(locals.info.staked, (uint64)locals.wallet);
+ }
+ if (state.mut().stakers.set(input.owner, locals.info) == NULL_INDEX)
+ {
+ return;
+ }
+ state.mut().totalStaked = sadd(state.get().totalStaked, (uint64)input.numberOfShares);
+ }
+};
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 39aa01b9..0147f515 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -34,6 +34,7 @@ add_executable(
# contract_qvault.cpp
# contract_qx.cpp
contract_qraffle.cpp
+ contract_qtreat.cpp
contract_random.cpp
contract_vottunbridge.cpp
# kangaroo_twelve.cpp
diff --git a/test/contract_qtreat.cpp b/test/contract_qtreat.cpp
new file mode 100644
index 00000000..321b17aa
--- /dev/null
+++ b/test/contract_qtreat.cpp
@@ -0,0 +1,1226 @@
+#define NO_UEFI
+
+#include
+#include "contract_testing.h"
+
+// MARKETPLACE_OWNER must match the hardcoded id set in QBAY::INITIALIZE() (Qbay.h).
+static const id QTREAT_TEST_MARKETPLACE_OWNER = ID(_R, _K, _D, _H, _C, _M, _R, _J, _Y, _C, _G, _K, _P, _D, _U, _Y, _R, _X, _G, _D, _Y, _Z, _C, _I, _Z, _I, _T, _A, _H, _Y, _O, _V, _G, _I, _U, _T, _K, _N, _D, _T, _E, _H, _P, _C, _C, _L, _W, _L, _Z, _X, _S, _H, _N, _F, _P, _D);
+
+static id getUser(uint64 i)
+{
+ return id(i + 1, i / 2 + 4, i + 10, i * 3 + 8);
+}
+
+// Exposes QTREAT's internal state for direct assertions beyond what the view functions return.
+class QTreatChecker : public QTREAT, public QTREAT::StateData
+{
+public:
+ bool getStaker(const id& user, StakerInfo& out) const
+ {
+ return stakers.get(user, out);
+ }
+ uint64 stakerCount() const { return stakers.population(); }
+ uint64 getTotalStaked() const { return totalStaked; }
+ uint64 getQtreatBonusPool() const { return qtreatBonusPool; }
+ uint64 getDividendFund() const { return dividendFund; }
+ uint64 getStakingFund() const { return stakingFund; }
+ uint64 getMiningFund() const { return miningFund; }
+ uint64 getDripQdogePool() const { return dripQdogePool; }
+ uint64 getAsicCatalogSize() const { return asicCatalogSize; }
+ uint64 getAsicCatalogLocked() const { return asicCatalogLocked; }
+ uint64 getDripStartEpoch() const { return dripStartEpoch; }
+ uint64 getTotalAsicCount() const { return totalAsicCount; }
+ uint64 getTotalMiningWeight() const { return totalMiningWeight; }
+ AsicRig getRig(sint64 idx) const { return asicRigs.get(idx); }
+ uint64 getAsicRigHighWater() const { return asicRigHighWater; }
+ id getAdminAddress() const { return adminAddress; }
+ Asset getQdogeToken() const { return qdogeToken; }
+ Asset getQtreatToken() const { return qtreatToken; }
+ uint64 generalAssetBalanceOf(const id& issuer, uint64 assetName) const
+ {
+ AssetKey key; key.issuer = issuer; key.assetName = assetName;
+ uint64 bal = 0;
+ generalAssetBalances.get(key, bal);
+ return bal;
+ }
+};
+
+class ContractTestingQtreat : protected ContractTesting
+{
+public:
+ id adminAddress;
+ id tokenIssuer; // shared issuer of both QTREAT and QDOGE assets
+ uint32 nextNftId = 0;
+
+ ContractTestingQtreat()
+ {
+ initEmptySpectrum();
+ initEmptyUniverse();
+ system.epoch = contractDescriptions[QTREAT_CONTRACT_INDEX].constructionEpoch;
+
+ INIT_CONTRACT(QX);
+ callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE);
+ INIT_CONTRACT(RANDOM);
+ callSystemProcedure(RANDOM_CONTRACT_INDEX, INITIALIZE);
+ INIT_CONTRACT(QBAY);
+ callSystemProcedure(QBAY_CONTRACT_INDEX, INITIALIZE);
+ INIT_CONTRACT(QTREAT);
+ callSystemProcedure(QTREAT_CONTRACT_INDEX, INITIALIZE);
+
+ adminAddress = getState()->getAdminAddress();
+ tokenIssuer = getState()->getQdogeToken().issuer;
+ EXPECT_EQ(tokenIssuer, getState()->getQtreatToken().issuer);
+
+ increaseEnergy(adminAddress, 1);
+ increaseEnergy(tokenIssuer, 1);
+
+ // Large pre-issued supplies of QDOGE and QTREAT so tests can freely fund users.
+ increaseEnergy(tokenIssuer, 2000000000ULL);
+ EXPECT_GT(issueAsset(tokenIssuer, QTREAT_QDOGE_ASSETNAME, 1000000000000LL, 0, 0), 0);
+ EXPECT_GT(issueAsset(tokenIssuer, QTREAT_TOKEN_ASSETNAME, 1000000000000LL, 0, 0), 0);
+
+ // tokenIssuer still possesses almost the entire pre-issued QTREAT-token supply
+ // (tests only ever hand out small slices of it). Left un-excluded, that dwarfs
+ // any test holder's balance and would absorb virtually 100% of every dividend
+ // payout by weight. Exclude it (reserving the last slot so tests are free to use
+ // the others for their own exclude-address scenarios).
+ setExcludeAddress(adminAddress, QTREAT_MAX_EXCLUDE_ADDRESSES - 1, tokenIssuer);
+
+ beginEpoch();
+ }
+
+ QTreatChecker* getState()
+ {
+ return (QTreatChecker*)contractStates[QTREAT_CONTRACT_INDEX];
+ }
+
+ void beginEpoch(bool expectSuccess = true) { callSystemProcedure(QTREAT_CONTRACT_INDEX, BEGIN_EPOCH, expectSuccess); }
+ void endEpoch(bool expectSuccess = true) { callSystemProcedure(QTREAT_CONTRACT_INDEX, END_EPOCH, expectSuccess); }
+
+ // Ends the current epoch, moves the clock forward, and begins the next one.
+ void advanceEpoch()
+ {
+ endEpoch();
+ system.epoch++;
+ beginEpoch();
+ }
+
+ // ---------------------------------------------------------------- QX helpers
+
+ sint64 issueAsset(const id& issuer, uint64 assetName, sint64 numberOfShares, uint64 unitOfMeasurement, sint8 numberOfDecimalPlaces)
+ {
+ QX::IssueAsset_input input{ assetName, numberOfShares, unitOfMeasurement, numberOfDecimalPlaces };
+ QX::IssueAsset_output output;
+ invokeUserProcedure(QX_CONTRACT_INDEX, 1, input, output, issuer, 1000000000ULL);
+ return output.issuedNumberOfShares;
+ }
+
+ sint64 xferOwnership(const id& issuer, uint64 assetName, const id& from, sint64 numberOfShares, const id& to)
+ {
+ QX::TransferShareOwnershipAndPossession_input input;
+ QX::TransferShareOwnershipAndPossession_output output;
+ input.assetName = assetName;
+ input.issuer = issuer;
+ input.newOwnerAndPossessor = to;
+ input.numberOfShares = numberOfShares;
+ increaseEnergy(from, 100);
+ invokeUserProcedure(QX_CONTRACT_INDEX, 2, input, output, from, 100);
+ return output.transferredNumberOfShares;
+ }
+
+ sint64 xferManagementRights(const id& issuer, uint64 assetName, uint32 newManagingContractIndex, sint64 numberOfShares, const id& currentOwner)
+ {
+ QX::TransferShareManagementRights_input input;
+ QX::TransferShareManagementRights_output output;
+ input.asset.assetName = assetName;
+ input.asset.issuer = issuer;
+ input.newManagingContractIndex = newManagingContractIndex;
+ input.numberOfShares = numberOfShares;
+ invokeUserProcedure(QX_CONTRACT_INDEX, 9, input, output, currentOwner, 0);
+ return output.transferredNumberOfShares;
+ }
+
+ // Give `user` `amount` QDOGE they fully own/possess (management still with QX).
+ void giveQdoge(const id& user, uint64 amount)
+ {
+ EXPECT_EQ(xferOwnership(tokenIssuer, QTREAT_QDOGE_ASSETNAME, tokenIssuer, (sint64)amount, user), (sint64)amount);
+ }
+
+ void giveQtreat(const id& user, uint64 amount)
+ {
+ EXPECT_EQ(xferOwnership(tokenIssuer, QTREAT_TOKEN_ASSETNAME, tokenIssuer, (sint64)amount, user), (sint64)amount);
+ }
+
+ // Give + stake in one step: transfers `amount` QDOGE to `user`, then the user
+ // transfers management rights to QTREAT (this is what real staking looks like).
+ sint64 stakeQdoge(const id& user, uint64 amount)
+ {
+ giveQdoge(user, amount);
+ return xferManagementRights(tokenIssuer, QTREAT_QDOGE_ASSETNAME, QTREAT_CONTRACT_INDEX, (sint64)amount, user);
+ }
+
+ // ---------------------------------------------------------------- QBAY helpers
+
+ void activateQbayMarket()
+ {
+ increaseEnergy(QTREAT_TEST_MARKETPLACE_OWNER, 1);
+ QBAY::changeStatusOfMarketPlace_input input{ 1 };
+ QBAY::changeStatusOfMarketPlace_output output;
+ invokeUserProcedure(QBAY_CONTRACT_INDEX, 17, input, output, QTREAT_TEST_MARKETPLACE_OWNER, 0);
+ EXPECT_EQ(output.returnCode, 0u);
+ }
+
+ // Mints a single free-standing NFT (no collection) owned by `user`. IDs are
+ // assigned sequentially by QBAY starting at 0, so the first call here returns 0.
+ uint32 mintNft(const id& user)
+ {
+ QBAY::mint_input input{};
+ input.royalty = 0;
+ input.collectionId = 0;
+ input.typeOfMint = 1;
+ QBAY::mint_output output;
+ increaseEnergy(user, QBAY_SINGLE_NFT_CREATE_FEE);
+ invokeUserProcedure(QBAY_CONTRACT_INDEX, 3, input, output, user, QBAY_SINGLE_NFT_CREATE_FEE);
+ EXPECT_EQ(output.returnCode, 0u);
+ return nextNftId++;
+ }
+
+ void transferNft(const id& user, uint32 NFTid, const id& receiver)
+ {
+ QBAY::transfer_input input{ receiver, NFTid };
+ QBAY::transfer_output output;
+ invokeUserProcedure(QBAY_CONTRACT_INDEX, 5, input, output, user, 0);
+ EXPECT_EQ(output.returnCode, 0u);
+ }
+
+ // ---------------------------------------------------------------- QTREAT view wrappers
+
+ QTREAT::GetStakingInfo_output getStakingInfo(const id& staker)
+ {
+ QTREAT::GetStakingInfo_input input{ staker };
+ QTREAT::GetStakingInfo_output output;
+ callFunction(QTREAT_CONTRACT_INDEX, 1, input, output);
+ return output;
+ }
+
+ QTREAT::GetPhaseInfo_output getPhaseInfo()
+ {
+ QTREAT::GetPhaseInfo_input input{};
+ QTREAT::GetPhaseInfo_output output;
+ callFunction(QTREAT_CONTRACT_INDEX, 2, input, output);
+ return output;
+ }
+
+ QTREAT::GetFunds_output getFunds()
+ {
+ QTREAT::GetFunds_input input{};
+ QTREAT::GetFunds_output output;
+ callFunction(QTREAT_CONTRACT_INDEX, 3, input, output);
+ return output;
+ }
+
+ QTREAT::GetRaffleInfo_output getRaffleInfo()
+ {
+ QTREAT::GetRaffleInfo_input input{};
+ QTREAT::GetRaffleInfo_output output;
+ callFunction(QTREAT_CONTRACT_INDEX, 4, input, output);
+ return output;
+ }
+
+ QTREAT::GetExcludeAddresses_output getExcludeAddresses()
+ {
+ QTREAT::GetExcludeAddresses_input input{};
+ QTREAT::GetExcludeAddresses_output output;
+ callFunction(QTREAT_CONTRACT_INDEX, 5, input, output);
+ return output;
+ }
+
+ QTREAT::GetNftInfo_output getNftInfo(const id& wallet)
+ {
+ QTREAT::GetNftInfo_input input{ wallet };
+ QTREAT::GetNftInfo_output output;
+ callFunction(QTREAT_CONTRACT_INDEX, 6, input, output);
+ return output;
+ }
+
+ QTREAT::GetMinerInfo_output getMinerInfo(const id& wallet)
+ {
+ QTREAT::GetMinerInfo_input input{ wallet };
+ QTREAT::GetMinerInfo_output output;
+ callFunction(QTREAT_CONTRACT_INDEX, 7, input, output);
+ return output;
+ }
+
+ QTREAT::GetAsicCatalogInfo_output getAsicCatalogInfo(uint32 nftId)
+ {
+ QTREAT::GetAsicCatalogInfo_input input{ nftId };
+ QTREAT::GetAsicCatalogInfo_output output;
+ callFunction(QTREAT_CONTRACT_INDEX, 8, input, output);
+ return output;
+ }
+
+ // ---------------------------------------------------------------- QTREAT procedure wrappers
+
+ uint32 depositDividends(const id& user, sint64 amount)
+ {
+ QTREAT::DepositDividends_input input{};
+ QTREAT::DepositDividends_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 1, input, output, user, amount);
+ return output.returnCode;
+ }
+
+ uint32 depositStakingFund(const id& user, sint64 amount)
+ {
+ QTREAT::DepositStakingFund_input input{};
+ QTREAT::DepositStakingFund_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 2, input, output, user, amount);
+ return output.returnCode;
+ }
+
+ QTREAT::RequestUnstake_output requestUnstake(const id& user, uint64 amount, sint64 fee)
+ {
+ QTREAT::RequestUnstake_input input{ amount };
+ QTREAT::RequestUnstake_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 3, input, output, user, fee);
+ return output;
+ }
+
+ uint32 finalizeUnstake(const id& user)
+ {
+ QTREAT::FinalizeUnstake_input input{};
+ QTREAT::FinalizeUnstake_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 4, input, output, user, 0);
+ return output.returnCode;
+ }
+
+ QTREAT::ClaimQtreatBonus_output claimQtreatBonus(const id& user, sint64 fee)
+ {
+ QTREAT::ClaimQtreatBonus_input input{};
+ QTREAT::ClaimQtreatBonus_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 5, input, output, user, fee);
+ return output;
+ }
+
+ // Admin path: transfers `amount` QTREAT-token management rights to QTREAT, then deposits.
+ uint32 depositQtreatTokensAsAdmin(uint64 amount)
+ {
+ giveQtreat(adminAddress, amount);
+ EXPECT_EQ(xferManagementRights(tokenIssuer, QTREAT_TOKEN_ASSETNAME, QTREAT_CONTRACT_INDEX, (sint64)amount, adminAddress), (sint64)amount);
+ QTREAT::DepositQtreatTokens_input input{ amount };
+ QTREAT::DepositQtreatTokens_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 6, input, output, adminAddress, 0);
+ return output.returnCode;
+ }
+
+ uint32 depositQtreatTokensRaw(const id& caller, uint64 amount)
+ {
+ QTREAT::DepositQtreatTokens_input input{ amount };
+ QTREAT::DepositQtreatTokens_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 6, input, output, caller, 0);
+ return output.returnCode;
+ }
+
+ uint32 depositGeneralAssetRaw(const id& caller, const Asset& asset, uint64 amount)
+ {
+ QTREAT::DepositGeneralAsset_input input{ asset, amount };
+ QTREAT::DepositGeneralAsset_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 7, input, output, caller, 0);
+ return output.returnCode;
+ }
+
+ uint32 setExcludeAddress(const id& caller, uint64 slot, const id& address)
+ {
+ QTREAT::SetExcludeAddress_input input{ slot, address };
+ QTREAT::SetExcludeAddress_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 8, input, output, caller, 0);
+ return output.returnCode;
+ }
+
+ uint32 revokeGeneralAsset(const id& caller, const Asset& asset, uint64 amount, sint64 fee)
+ {
+ QTREAT::RevokeGeneralAsset_input input{ asset, amount };
+ QTREAT::RevokeGeneralAsset_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 9, input, output, caller, fee);
+ return output.returnCode;
+ }
+
+ uint32 releaseManagedShares(const id& caller, const Asset& asset, uint64 amount, sint64 fee)
+ {
+ QTREAT::ReleaseManagedShares_input input{ asset, amount };
+ QTREAT::ReleaseManagedShares_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 10, input, output, caller, fee);
+ return output.returnCode;
+ }
+
+ uint32 depositMiningFund(const id& user, sint64 amount)
+ {
+ QTREAT::DepositMiningFund_input input{};
+ QTREAT::DepositMiningFund_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 11, input, output, user, amount);
+ return output.returnCode;
+ }
+
+ QTREAT::LoadAsicPart_output loadAsicPart(const id& caller, uint32 nftId, uint8 category, uint8 rarity)
+ {
+ QTREAT::LoadAsicPart_input input{ nftId, category, rarity };
+ QTREAT::LoadAsicPart_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 12, input, output, caller, 0);
+ return output;
+ }
+
+ QTREAT::RegisterAsic_output registerAsic(const id& caller, uint32 m, uint32 c, uint32 p, uint32 f)
+ {
+ QTREAT::RegisterAsic_input input{ m, c, p, f };
+ QTREAT::RegisterAsic_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 13, input, output, caller, 0);
+ return output;
+ }
+
+ uint32 unregisterAsic(const id& caller, uint64 rigIndex)
+ {
+ QTREAT::UnregisterAsic_input input{ rigIndex };
+ QTREAT::UnregisterAsic_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 14, input, output, caller, 0);
+ return output.returnCode;
+ }
+
+ uint32 depositDripQdogeAsAdmin(uint64 amount)
+ {
+ // Admin transfers QDOGE management rights to QTREAT without becoming a staker
+ // (PRE_ACQUIRE_SHARES / POST_ACQUIRE_SHARES both bypass the admin address).
+ giveQdoge(adminAddress, amount);
+ EXPECT_EQ(xferManagementRights(tokenIssuer, QTREAT_QDOGE_ASSETNAME, QTREAT_CONTRACT_INDEX, (sint64)amount, adminAddress), (sint64)amount);
+ QTREAT::DepositDripQdoge_input input{ amount };
+ QTREAT::DepositDripQdoge_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 15, input, output, adminAddress, 0);
+ return output.returnCode;
+ }
+
+ uint32 depositDripQdogeRaw(const id& caller, uint64 amount)
+ {
+ QTREAT::DepositDripQdoge_input input{ amount };
+ QTREAT::DepositDripQdoge_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 15, input, output, caller, 0);
+ return output.returnCode;
+ }
+
+ uint32 setMiningRate(const id& caller, uint64 rate)
+ {
+ QTREAT::SetMiningRate_input input{ rate };
+ QTREAT::SetMiningRate_output output;
+ invokeUserProcedure(QTREAT_CONTRACT_INDEX, 16, input, output, caller, 0);
+ return output.returnCode;
+ }
+
+ // Loads the full 400-part ASIC catalog. Reserves nftId==category (0,1,2,3) as the
+ // single COMMON-rarity part of each category, so a real 4-part rig can be built
+ // from just 4 minted QBAY NFTs (ids 0..3) without needing 400 real NFTs.
+ void loadFullAsicCatalog()
+ {
+ static const uint64 budgets[5] = { 40, 30, 18, 10, 2 };
+ uint32 next = 10000;
+ for (uint8 cat = 0; cat < 4; cat++)
+ {
+ for (uint8 rarity = 0; rarity < 5; rarity++)
+ {
+ for (uint64 n = 0; n < budgets[rarity]; n++)
+ {
+ uint32 id = (rarity == 0 && n == 0) ? (uint32)cat : next++;
+ auto out = loadAsicPart(adminAddress, id, cat, rarity);
+ EXPECT_EQ(out.returnCode, QTREAT_OK);
+ }
+ }
+ }
+ EXPECT_EQ(getState()->getAsicCatalogSize(), QTREAT_ASIC_PARTS_TOTAL);
+ EXPECT_EQ(getState()->getAsicCatalogLocked(), 1u);
+ }
+};
+
+// =============================================================================
+// Basic state / views
+// =============================================================================
+
+TEST(ContractQtreat, AdminAddressRoundTripsToExpectedIdentity)
+{
+ // Confirms the ID(...) letter macro in INITIALIZE() encodes exactly the intended
+ // 60-character address, checksum included (the checksum is a K12 hash of the
+ // underlying public key, not derivable by inspection of the 56 identity letters
+ // alone - this is the only reliable way to catch a transcription error here).
+ ContractTestingQtreat t;
+ std::ostringstream oss;
+ oss << t.getState()->getAdminAddress();
+ EXPECT_EQ(oss.str(), "QTREATZZIVFYQAIBKCZPSHGLIRMALZKHEWAPFLFXJAMDAXMGTBKQVXHHDHUD");
+}
+
+TEST(ContractQtreat, InitialStateAfterFirstBeginEpoch)
+{
+ ContractTestingQtreat t;
+
+ auto phase = t.getPhaseInfo();
+ EXPECT_EQ(phase.stakingStartEpoch, (uint64)contractDescriptions[QTREAT_CONTRACT_INDEX].constructionEpoch);
+ EXPECT_EQ(phase.currentPhase, 1u);
+ EXPECT_EQ(phase.epochsRemaining, QTREAT_TOTAL_REWARD_EPOCHS);
+
+ auto raffle = t.getRaffleInfo();
+ EXPECT_EQ(raffle.totalRaffleAwarded, 0u);
+ EXPECT_EQ(raffle.lastRaffleWinner, NULL_ID);
+ EXPECT_EQ(raffle.raffleEpochsRemaining, QTREAT_RAFFLE_EPOCHS);
+
+ auto funds = t.getFunds();
+ EXPECT_EQ(funds.dividendFund, 0u);
+ EXPECT_EQ(funds.stakingFund, 0u);
+ EXPECT_EQ(funds.qtreatBonusPool, 0u);
+ EXPECT_EQ(funds.miningFund, 0u);
+
+ auto info = t.getStakingInfo(getUser(1));
+ EXPECT_EQ(info.isStaker, 0u);
+ EXPECT_EQ(info.totalStaked, 0u);
+
+ // Slot QTREAT_MAX_EXCLUDE_ADDRESSES-1 is reserved by the test fixture itself to
+ // exclude tokenIssuer from dividend/NFT eligibility (see ContractTestingQtreat ctor).
+ auto excl = t.getExcludeAddresses();
+ for (sint64 i = 0; i < (sint64)QTREAT_MAX_EXCLUDE_ADDRESSES - 1; i++)
+ EXPECT_EQ(excl.addresses.get(i), NULL_ID);
+ EXPECT_EQ(excl.addresses.get((sint64)QTREAT_MAX_EXCLUDE_ADDRESSES - 1), t.tokenIssuer);
+
+ EXPECT_EQ(t.getNftInfo(getUser(1)).nftCount, 0u);
+ EXPECT_EQ(t.getMinerInfo(getUser(1)).asicCount, 0u);
+ EXPECT_EQ(t.getAsicCatalogInfo(0).isCataloged, 0u);
+}
+
+TEST(ContractQtreat, DepositFundsAccumulateAndRejectZero)
+{
+ ContractTestingQtreat t;
+ id user = getUser(1);
+ increaseEnergy(user, 1000);
+
+ EXPECT_EQ(t.depositDividends(user, 0), QTREAT_ERR_ZERO_AMOUNT);
+ EXPECT_EQ(t.depositStakingFund(user, 0), QTREAT_ERR_ZERO_AMOUNT);
+ EXPECT_EQ(t.depositMiningFund(user, 0), QTREAT_ERR_ZERO_AMOUNT);
+
+ EXPECT_EQ(t.depositDividends(user, 100), QTREAT_OK);
+ EXPECT_EQ(t.depositStakingFund(user, 200), QTREAT_OK);
+ EXPECT_EQ(t.depositMiningFund(user, 300), QTREAT_OK);
+
+ auto funds = t.getFunds();
+ EXPECT_EQ(funds.dividendFund, 100u);
+ EXPECT_EQ(funds.stakingFund, 200u);
+ EXPECT_EQ(funds.miningFund, 300u);
+}
+
+TEST(ContractQtreat, PlainTransferRoutesToDividendFund)
+{
+ ContractTestingQtreat t;
+ // POST_INCOMING_TRANSFER credits standardTransaction/qpiTransfer/revenueDonation to dividendFund.
+ // invokeUserProcedure's fee path (amount>0 triggers POST_INCOMING_TRANSFER with procedureTransaction,
+ // which is intentionally NOT one of the routed types) is exercised implicitly by DepositDividends above;
+ // here we confirm GetFunds reflects a single accumulation path deterministically.
+ id user = getUser(2);
+ increaseEnergy(user, 500);
+ EXPECT_EQ(t.depositDividends(user, 500), QTREAT_OK);
+ EXPECT_EQ(t.getFunds().dividendFund, 500u);
+}
+
+// =============================================================================
+// Staking / unstaking
+// =============================================================================
+
+TEST(ContractQtreat, StakingEnforcesMinimum)
+{
+ ContractTestingQtreat t;
+ id user = getUser(1);
+ increaseEnergy(user, 1000);
+
+ // Below minimum: PRE_ACQUIRE_SHARES denies the transfer, so QX reports 0 shares moved.
+ EXPECT_EQ(t.stakeQdoge(user, QTREAT_MIN_STAKE - 1), 0);
+ EXPECT_EQ(t.getStakingInfo(user).isStaker, 0u);
+
+ // At minimum: succeeds.
+ EXPECT_EQ(t.stakeQdoge(user, QTREAT_MIN_STAKE), (sint64)QTREAT_MIN_STAKE);
+ auto info = t.getStakingInfo(user);
+ EXPECT_EQ(info.isStaker, 1u);
+ EXPECT_EQ(info.staked, QTREAT_MIN_STAKE);
+ EXPECT_EQ(t.getState()->getTotalStaked(), QTREAT_MIN_STAKE);
+}
+
+TEST(ContractQtreat, StakingAccumulatesAcrossMultipleTransfers)
+{
+ ContractTestingQtreat t;
+ id user = getUser(1);
+ increaseEnergy(user, 1000);
+ EXPECT_EQ(t.stakeQdoge(user, QTREAT_MIN_STAKE), (sint64)QTREAT_MIN_STAKE);
+ EXPECT_EQ(t.stakeQdoge(user, 5000000ULL), 5000000);
+ EXPECT_EQ(t.getStakingInfo(user).staked, QTREAT_MIN_STAKE + 5000000ULL);
+ EXPECT_EQ(t.getState()->getTotalStaked(), QTREAT_MIN_STAKE + 5000000ULL);
+}
+
+TEST(ContractQtreat, RequestUnstakeValidations)
+{
+ ContractTestingQtreat t;
+ id staker = getUser(1);
+ id nonStaker = getUser(2);
+ increaseEnergy(nonStaker, 1000);
+ increaseEnergy(staker, 1000);
+
+ EXPECT_EQ(t.stakeQdoge(staker, QTREAT_MIN_STAKE * 2), (sint64)(QTREAT_MIN_STAKE * 2));
+
+ // Not a staker at all.
+ EXPECT_EQ(t.requestUnstake(nonStaker, 1, QTREAT_QX_TRANSFER_FEE).returnCode, QTREAT_ERR_NOT_STAKER);
+
+ // Zero / excess amount.
+ EXPECT_EQ(t.requestUnstake(staker, 0, QTREAT_QX_TRANSFER_FEE).returnCode, QTREAT_ERR_INSUFFICIENT_STAKE);
+ EXPECT_EQ(t.requestUnstake(staker, QTREAT_MIN_STAKE * 2 + 1, QTREAT_QX_TRANSFER_FEE).returnCode, QTREAT_ERR_INSUFFICIENT_STAKE);
+
+ // Leaving a dust remainder below the minimum is rejected.
+ EXPECT_EQ(t.requestUnstake(staker, QTREAT_MIN_STAKE * 2 - 1, QTREAT_QX_TRANSFER_FEE).returnCode, QTREAT_ERR_BELOW_MIN_STAKE);
+
+ // Fee too low.
+ EXPECT_EQ(t.requestUnstake(staker, QTREAT_MIN_STAKE, QTREAT_QX_TRANSFER_FEE - 1).returnCode, QTREAT_ERR_INSUFFICIENT_FEE);
+
+ // Successful partial unstake with fee overpayment refunded.
+ long long balBefore = getBalance(staker);
+ auto out = t.requestUnstake(staker, QTREAT_MIN_STAKE, QTREAT_QX_TRANSFER_FEE + 50);
+ EXPECT_EQ(out.returnCode, QTREAT_OK);
+ EXPECT_EQ(getBalance(staker), balBefore - QTREAT_QX_TRANSFER_FEE);
+
+ auto info = t.getStakingInfo(staker);
+ EXPECT_EQ(info.staked, QTREAT_MIN_STAKE);
+ EXPECT_EQ(info.unstakeAmount, QTREAT_MIN_STAKE);
+
+ // A second unstake request while one is pending is rejected.
+ EXPECT_EQ(t.requestUnstake(staker, 1, QTREAT_QX_TRANSFER_FEE).returnCode, QTREAT_ERR_UNSTAKE_PENDING);
+}
+
+TEST(ContractQtreat, FinalizeUnstakeRespectsDelayAndReleasesQdoge)
+{
+ ContractTestingQtreat t;
+ id staker = getUser(1);
+ increaseEnergy(staker, 1000);
+ EXPECT_EQ(t.stakeQdoge(staker, QTREAT_MIN_STAKE), (sint64)QTREAT_MIN_STAKE);
+ ASSERT_EQ(t.requestUnstake(staker, QTREAT_MIN_STAKE, QTREAT_QX_TRANSFER_FEE).returnCode, QTREAT_OK);
+
+ // Not ready immediately.
+ EXPECT_EQ(t.finalizeUnstake(staker), QTREAT_ERR_UNSTAKE_NOT_READY);
+
+ for (uint64 i = 0; i < QTREAT_UNSTAKE_DELAY_EPOCHS; i++)
+ t.advanceEpoch();
+
+ sint64 walletBefore = numberOfPossessedShares(QTREAT_QDOGE_ASSETNAME, t.tokenIssuer, staker, staker, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX);
+ EXPECT_EQ(t.finalizeUnstake(staker), QTREAT_OK);
+ sint64 walletAfter = numberOfPossessedShares(QTREAT_QDOGE_ASSETNAME, t.tokenIssuer, staker, staker, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX);
+ EXPECT_EQ(walletAfter - walletBefore, (sint64)QTREAT_MIN_STAKE);
+
+ // Staker record is dropped once fully unwound (no stake, no unstake, no bonus).
+ QTREAT::StakerInfo info;
+ EXPECT_FALSE(t.getState()->getStaker(staker, info));
+}
+
+TEST(ContractQtreat, EndEpochAutoReleasesMaturedUnstakes)
+{
+ ContractTestingQtreat t;
+ id staker = getUser(1);
+ increaseEnergy(staker, 1000);
+ EXPECT_EQ(t.stakeQdoge(staker, QTREAT_MIN_STAKE), (sint64)QTREAT_MIN_STAKE);
+ ASSERT_EQ(t.requestUnstake(staker, QTREAT_MIN_STAKE, QTREAT_QX_TRANSFER_FEE).returnCode, QTREAT_OK);
+
+ // advanceEpoch() runs END_EPOCH BEFORE bumping the epoch counter, so maturity
+ // (epoch >= unstakeEpoch + DELAY_EPOCHS) is only reached by the (DELAY_EPOCHS+1)-th
+ // END_EPOCH call — one more than the number of epochs elapsed.
+ for (uint64 i = 0; i <= QTREAT_UNSTAKE_DELAY_EPOCHS; i++)
+ t.advanceEpoch();
+
+ // END_EPOCH itself (of the tick where the delay has elapsed) auto-releases; no manual
+ // FinalizeUnstake call needed.
+ sint64 wallet = numberOfPossessedShares(QTREAT_QDOGE_ASSETNAME, t.tokenIssuer, staker, staker, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX);
+ EXPECT_EQ(wallet, (sint64)QTREAT_MIN_STAKE);
+ EXPECT_EQ(t.getStakingInfo(staker).unstakeAmount, 0u);
+}
+
+// =============================================================================
+// Growth streak / progressive bonus
+// =============================================================================
+
+TEST(ContractQtreat, GrowthStreakIncrementsWhenStakeGrowsAndResetsOtherwise)
+{
+ ContractTestingQtreat t;
+ id grower = getUser(1);
+ id flat = getUser(2);
+ increaseEnergy(grower, 1000);
+ increaseEnergy(flat, 1000);
+
+ EXPECT_EQ(t.stakeQdoge(grower, QTREAT_MIN_STAKE), (sint64)QTREAT_MIN_STAKE);
+ EXPECT_EQ(t.stakeQdoge(flat, QTREAT_MIN_STAKE), (sint64)QTREAT_MIN_STAKE);
+
+ // Progressive bonus only starts accruing after QTREAT_PROGRESSIVE_START_DELAY_EPOCHS.
+ for (uint64 i = 0; i < QTREAT_PROGRESSIVE_START_DELAY_EPOCHS; i++)
+ {
+ EXPECT_EQ(t.stakeQdoge(grower, QTREAT_PROGRESSIVE_MIN_STEP), (sint64)QTREAT_PROGRESSIVE_MIN_STEP);
+ t.advanceEpoch();
+ }
+ EXPECT_EQ(t.getStakingInfo(grower).growthStreak, 0u);
+ EXPECT_EQ(t.getStakingInfo(flat).growthStreak, 0u);
+
+ // Now growing epoch over epoch increments the streak.
+ EXPECT_EQ(t.stakeQdoge(grower, QTREAT_PROGRESSIVE_MIN_STEP), (sint64)QTREAT_PROGRESSIVE_MIN_STEP);
+ t.advanceEpoch();
+ EXPECT_EQ(t.getStakingInfo(grower).growthStreak, 1u);
+ EXPECT_EQ(t.getStakingInfo(flat).growthStreak, 0u);
+
+ EXPECT_EQ(t.stakeQdoge(grower, QTREAT_PROGRESSIVE_MIN_STEP), (sint64)QTREAT_PROGRESSIVE_MIN_STEP);
+ t.advanceEpoch();
+ EXPECT_EQ(t.getStakingInfo(grower).growthStreak, 2u);
+
+ // A flat epoch (no growth) resets the streak to zero.
+ t.advanceEpoch();
+ EXPECT_EQ(t.getStakingInfo(grower).growthStreak, 0u);
+}
+
+TEST(ContractQtreat, StakingRewardWeightedByGrowthStreak)
+{
+ ContractTestingQtreat t;
+ id grower = getUser(1);
+ id flat = getUser(2);
+ increaseEnergy(grower, 1000);
+ increaseEnergy(flat, 1000);
+
+ EXPECT_EQ(t.stakeQdoge(grower, QTREAT_MIN_STAKE), (sint64)QTREAT_MIN_STAKE);
+ EXPECT_EQ(t.stakeQdoge(flat, QTREAT_MIN_STAKE), (sint64)QTREAT_MIN_STAKE);
+
+ for (uint64 i = 0; i < QTREAT_PROGRESSIVE_START_DELAY_EPOCHS; i++)
+ t.advanceEpoch();
+
+ EXPECT_EQ(t.stakeQdoge(grower, QTREAT_PROGRESSIVE_MIN_STEP), (sint64)QTREAT_PROGRESSIVE_MIN_STEP);
+
+ id funder = getUser(3);
+ increaseEnergy(funder, QTREAT_STAKER_REWARD_PER_EPOCH * 2);
+ EXPECT_EQ(t.depositStakingFund(funder, (sint64)QTREAT_STAKER_REWARD_PER_EPOCH * 2), QTREAT_OK);
+
+ long long growerBefore = getBalance(grower);
+ long long flatBefore = getBalance(flat);
+ t.advanceEpoch(); // pays out with grower now on a 1-epoch streak (+25 permille weight)
+
+ long long growerReward = getBalance(grower) - growerBefore;
+ long long flatReward = getBalance(flat) - flatBefore;
+ // Grower staked slightly more AND has a streak bonus, so must earn strictly more.
+ EXPECT_GT(growerReward, flatReward);
+}
+
+// =============================================================================
+// Loyalty bonus / raffle
+// =============================================================================
+
+TEST(ContractQtreat, LoyaltyBonusAccruesAndAutoDelivers)
+{
+ // Deliberately leave stakingFund/qtreatBonusPool at zero for the 12-epoch accrual
+ // phase: RAFFLE also requires qtreatBonusPool>0 to run, and a funded stakingFund
+ // would ALSO be swept by that same epoch's staking-reward payout (this staker holds
+ // 100% of the weight) before any delivery loop could see it. Zero-funding both keeps
+ // this phase a clean, deterministic test of the bonusEpochs/bonusAwarded counters
+ // alone, with neither the reward payout nor the raffle able to interfere.
+ ContractTestingQtreat t;
+ id staker = getUser(1);
+ increaseEnergy(staker, 1000);
+ EXPECT_EQ(t.stakeQdoge(staker, QTREAT_BONUS_THRESHOLD), (sint64)QTREAT_BONUS_THRESHOLD);
+
+ for (uint64 i = 0; i < QTREAT_BONUS_INTERVAL_EPOCHS; i++)
+ t.advanceEpoch();
+
+ auto info = t.getStakingInfo(staker);
+ EXPECT_EQ(info.bonusAwarded, 1u);
+ EXPECT_EQ(info.pendingBonus, 1u); // earned, but undelivered: qtreatBonusPool is still empty
+
+ // Now fund both pools and let one more END_EPOCH run the auto-delivery loop. (The
+ // raffle may also fire and hand out an extra token in this same epoch - that's fine,
+ // this only asserts the loyalty bonus's own pendingBonus got cleared and the staker
+ // received at least their 1 bonus token.)
+ id funder = getUser(9);
+ increaseEnergy(funder, QTREAT_STAKER_REWARD_PER_EPOCH + QTREAT_RAFFLE_ENTROPY_FEE + 1000);
+ EXPECT_EQ(t.depositStakingFund(funder, (sint64)(QTREAT_STAKER_REWARD_PER_EPOCH + QTREAT_RAFFLE_ENTROPY_FEE + 1000)), QTREAT_OK);
+ EXPECT_EQ(t.depositQtreatTokensAsAdmin(10), QTREAT_OK);
+
+ t.advanceEpoch();
+
+ EXPECT_EQ(t.getStakingInfo(staker).pendingBonus, 0u);
+ sint64 held = numberOfPossessedShares(QTREAT_TOKEN_ASSETNAME, t.tokenIssuer, staker, staker, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX);
+ EXPECT_GE(held, 1);
+}
+
+TEST(ContractQtreat, LoyaltyBonusManualClaimFallback)
+{
+ ContractTestingQtreat t;
+ id staker = getUser(1);
+ increaseEnergy(staker, 1000);
+ EXPECT_EQ(t.stakeQdoge(staker, QTREAT_BONUS_THRESHOLD), (sint64)QTREAT_BONUS_THRESHOLD);
+
+ // No pending bonus yet.
+ EXPECT_EQ(t.claimQtreatBonus(staker, QTREAT_QX_TRANSFER_FEE).returnCode, QTREAT_ERR_NO_PENDING_BONUS);
+
+ // Fund the staking fund only (delivery fee available) but leave the bonus pool empty,
+ // so END_EPOCH auto-delivery is skipped and the bonus stays pending.
+ id funder = getUser(9);
+ increaseEnergy(funder, QTREAT_STAKER_REWARD_PER_EPOCH * QTREAT_BONUS_INTERVAL_EPOCHS);
+ EXPECT_EQ(t.depositStakingFund(funder, (sint64)(QTREAT_STAKER_REWARD_PER_EPOCH * QTREAT_BONUS_INTERVAL_EPOCHS)), QTREAT_OK);
+
+ for (uint64 i = 0; i < QTREAT_BONUS_INTERVAL_EPOCHS; i++)
+ t.advanceEpoch();
+
+ EXPECT_EQ(t.getStakingInfo(staker).pendingBonus, 1u);
+
+ // Bonus pool still empty: claim fails.
+ EXPECT_EQ(t.claimQtreatBonus(staker, QTREAT_QX_TRANSFER_FEE).returnCode, QTREAT_ERR_BONUS_POOL_EMPTY);
+
+ EXPECT_EQ(t.depositQtreatTokensAsAdmin(10), QTREAT_OK);
+ auto claim = t.claimQtreatBonus(staker, QTREAT_QX_TRANSFER_FEE);
+ EXPECT_EQ(claim.returnCode, QTREAT_OK);
+ EXPECT_EQ(claim.claimed, 1u);
+ EXPECT_EQ(t.getStakingInfo(staker).pendingBonus, 0u);
+}
+
+TEST(ContractQtreat, RaffleFallsBackToChainStateEntropyAndPaysWinner)
+{
+ ContractTestingQtreat t;
+ id staker = getUser(1);
+ increaseEnergy(staker, 1000);
+ EXPECT_EQ(t.stakeQdoge(staker, QTREAT_MIN_STAKE), (sint64)QTREAT_MIN_STAKE);
+
+ // The staking-reward payout runs (and fully drains stakingFund, since this staker
+ // holds 100% of the weight) BEFORE the raffle block in the same END_EPOCH call, so
+ // stakingFund must cover a full epoch of staking reward *plus* the entropy fee.
+ id funder = getUser(9);
+ sint64 stakingFundTopUp = (sint64)QTREAT_STAKER_REWARD_PER_EPOCH + (sint64)QTREAT_RAFFLE_ENTROPY_FEE + 1000;
+ increaseEnergy(funder, stakingFundTopUp);
+ EXPECT_EQ(t.depositStakingFund(funder, stakingFundTopUp), QTREAT_OK);
+ EXPECT_EQ(t.depositQtreatTokensAsAdmin(5), QTREAT_OK);
+
+ // RANDOM has no registered entropy providers, so BuyEntropy refunds (all-zero entropy);
+ // QTREAT must still pick a winner via the chain-state-digest fallback (single eligible
+ // staker here, so the outcome is deterministic).
+ t.advanceEpoch();
+
+ auto raffle = t.getRaffleInfo();
+ EXPECT_EQ(raffle.totalRaffleAwarded, 1u);
+ EXPECT_EQ(raffle.lastRaffleWinner, staker);
+
+ sint64 held = numberOfPossessedShares(QTREAT_TOKEN_ASSETNAME, t.tokenIssuer, staker, staker, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX);
+ EXPECT_EQ(held, 1);
+}
+
+TEST(ContractQtreat, RaffleSkipsIneligibleUnstakingStaker)
+{
+ ContractTestingQtreat t;
+ id staker = getUser(1);
+ increaseEnergy(staker, 1000);
+ EXPECT_EQ(t.stakeQdoge(staker, QTREAT_MIN_STAKE), (sint64)QTREAT_MIN_STAKE);
+ ASSERT_EQ(t.requestUnstake(staker, QTREAT_MIN_STAKE, QTREAT_QX_TRANSFER_FEE).returnCode, QTREAT_OK);
+ // Fully unstaked: staked==0, so no longer an eligible raffle participant either.
+
+ id funder = getUser(9);
+ increaseEnergy(funder, QTREAT_RAFFLE_ENTROPY_FEE + 1000);
+ EXPECT_EQ(t.depositStakingFund(funder, (sint64)QTREAT_RAFFLE_ENTROPY_FEE + 1000), QTREAT_OK);
+ EXPECT_EQ(t.depositQtreatTokensAsAdmin(5), QTREAT_OK);
+
+ t.advanceEpoch();
+ EXPECT_EQ(t.getRaffleInfo().totalRaffleAwarded, 0u);
+}
+
+// =============================================================================
+// Dividends
+// =============================================================================
+
+TEST(ContractQtreat, DividendsSplitBetweenShareholdersAndMinBalanceHolders)
+{
+ ContractTestingQtreat t;
+ id holder = getUser(1);
+ increaseEnergy(holder, 1000);
+ t.giveQtreat(holder, 1000);
+
+ // Start a fresh epoch so BEGIN_EPOCH's snapshot picks up the holder's balance.
+ t.advanceEpoch();
+
+ id funder = getUser(9);
+ increaseEnergy(funder, 1000000);
+ EXPECT_EQ(t.depositDividends(funder, 1000000), QTREAT_OK);
+
+ long long before = getBalance(holder);
+ t.advanceEpoch();
+ long long after = getBalance(holder);
+
+ // Shareholder cut is paid out per-computor (dust lost to integer division stays
+ // undistributed to shareholders); the sole holder receives the entire remainder.
+ uint64 shareholderShare = 1000000ULL * QTREAT_DIVIDEND_SHAREHOLDER_PERMILLE / 1000;
+ uint64 paidShareholders = (shareholderShare / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS;
+ uint64 expectedHolderShare = 1000000ULL - paidShareholders;
+ EXPECT_EQ((uint64)(after - before), expectedHolderShare);
+ EXPECT_GT(t.getState()->totalShareholderDividends, 0u);
+}
+
+TEST(ContractQtreat, DividendsUseMinOfBeginAndEndBalance)
+{
+ ContractTestingQtreat t;
+ id holderA = getUser(1); // keeps full balance all epoch
+ id holderB = getUser(2); // sells half mid-epoch
+ increaseEnergy(holderA, 1000);
+ increaseEnergy(holderB, 1000);
+ t.giveQtreat(holderA, 1000);
+ t.giveQtreat(holderB, 1000);
+
+ t.advanceEpoch(); // snapshot begin balances: both at 1000
+
+ // holderB moves half away mid-epoch -> end balance 500, min(begin,end)=500.
+ id sink = getUser(3);
+ EXPECT_EQ(t.xferOwnership(t.tokenIssuer, QTREAT_TOKEN_ASSETNAME, holderB, 500, sink), 500);
+
+ id funder = getUser(9);
+ increaseEnergy(funder, 3000000);
+ EXPECT_EQ(t.depositDividends(funder, 3000000), QTREAT_OK);
+
+ long long beforeA = getBalance(holderA);
+ long long beforeB = getBalance(holderB);
+ t.advanceEpoch();
+ long long rewardA = getBalance(holderA) - beforeA;
+ long long rewardB = getBalance(holderB) - beforeB;
+
+ // holderA's effective weight (1000) is double holderB's (500 = min(1000,500)).
+ EXPECT_GT(rewardA, rewardB);
+ EXPECT_NEAR((double)rewardA / (double)rewardB, 2.0, 0.05);
+}
+
+TEST(ContractQtreat, DividendsExcludeConfiguredAddress)
+{
+ ContractTestingQtreat t;
+ id excluded = getUser(1);
+ id normal = getUser(2);
+ increaseEnergy(excluded, 1000);
+ increaseEnergy(normal, 1000);
+ t.giveQtreat(excluded, 1000);
+ t.giveQtreat(normal, 1000);
+
+ EXPECT_EQ(t.setExcludeAddress(t.adminAddress, 0, excluded), QTREAT_OK);
+ // Non-admin cannot set exclusions.
+ EXPECT_EQ(t.setExcludeAddress(normal, 1, normal), QTREAT_ERR_ACCESS_DENIED);
+
+ t.advanceEpoch();
+
+ id funder = getUser(9);
+ increaseEnergy(funder, 1000000);
+ EXPECT_EQ(t.depositDividends(funder, 1000000), QTREAT_OK);
+
+ long long beforeExcluded = getBalance(excluded);
+ long long beforeNormal = getBalance(normal);
+ t.advanceEpoch();
+
+ EXPECT_EQ(getBalance(excluded), beforeExcluded);
+ EXPECT_GT(getBalance(normal), beforeNormal);
+}
+
+// =============================================================================
+// General asset custody
+// =============================================================================
+
+TEST(ContractQtreat, GeneralAssetCustodyIsAdminGatedAndRoundTrips)
+{
+ ContractTestingQtreat t;
+ id nonAdmin = getUser(1);
+ increaseEnergy(nonAdmin, 1000);
+
+ id assetIssuer = getUser(50);
+ increaseEnergy(assetIssuer, 2000000000ULL);
+ uint64 assetName = assetNameFromString("TASSET");
+ EXPECT_GT(t.issueAsset(assetIssuer, assetName, 1000000, 0, 0), 0);
+
+ Asset asset; asset.issuer = assetIssuer; asset.assetName = assetName;
+
+ // Give the admin the shares, but do NOT transfer management rights: DepositGeneralAsset
+ // should refuse a non-admin caller regardless, and the admin call needs mgmt rights first.
+ t.xferOwnership(assetIssuer, assetName, assetIssuer, 5000, t.adminAddress);
+ EXPECT_EQ(t.depositGeneralAssetRaw(nonAdmin, asset, 1000), QTREAT_ERR_ACCESS_DENIED);
+
+ EXPECT_EQ(t.xferManagementRights(assetIssuer, assetName, QTREAT_CONTRACT_INDEX, 5000, t.adminAddress), 5000);
+ EXPECT_EQ(t.depositGeneralAssetRaw(t.adminAddress, asset, 5000), QTREAT_OK);
+ EXPECT_EQ(t.getState()->generalAssetBalanceOf(assetIssuer, assetName), 5000u);
+
+ // Revoke: non-admin rejected, over-revoke rejected, full revoke clears the entry.
+ EXPECT_EQ(t.revokeGeneralAsset(nonAdmin, asset, 1000, QTREAT_QX_TRANSFER_FEE), QTREAT_ERR_ACCESS_DENIED);
+ increaseEnergy(t.adminAddress, QTREAT_QX_TRANSFER_FEE * 2);
+ EXPECT_EQ(t.revokeGeneralAsset(t.adminAddress, asset, 6000, QTREAT_QX_TRANSFER_FEE), QTREAT_ERR_INVALID_INPUT);
+ EXPECT_EQ(t.revokeGeneralAsset(t.adminAddress, asset, 5000, QTREAT_QX_TRANSFER_FEE), QTREAT_OK);
+ EXPECT_EQ(t.getState()->generalAssetBalanceOf(assetIssuer, assetName), 0u);
+
+ sint64 backInWallet = numberOfPossessedShares(assetName, assetIssuer, t.adminAddress, t.adminAddress, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX);
+ EXPECT_EQ(backInWallet, 5000);
+}
+
+TEST(ContractQtreat, ReleaseManagedSharesAllowsAdminToPullBackUnaccountedQdoge)
+{
+ ContractTestingQtreat t;
+ // Admin transfers QDOGE management to QTREAT without staking (bypass path).
+ t.giveQdoge(t.adminAddress, 1000000);
+ EXPECT_EQ(t.xferManagementRights(t.tokenIssuer, QTREAT_QDOGE_ASSETNAME, QTREAT_CONTRACT_INDEX, 1000000, t.adminAddress), 1000000);
+
+ increaseEnergy(t.adminAddress, QTREAT_QX_TRANSFER_FEE);
+ EXPECT_EQ(t.releaseManagedShares(t.adminAddress, t.getState()->getQdogeToken(), 1000000, QTREAT_QX_TRANSFER_FEE), QTREAT_OK);
+
+ sint64 backInWallet = numberOfPossessedShares(QTREAT_QDOGE_ASSETNAME, t.tokenIssuer, t.adminAddress, t.adminAddress, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX);
+ EXPECT_EQ(backInWallet, 1000000);
+}
+
+TEST(ContractQtreat, ReleaseManagedSharesRejectsReleasingActiveStake)
+{
+ ContractTestingQtreat t;
+ id staker = getUser(1);
+ increaseEnergy(staker, QTREAT_QX_TRANSFER_FEE * 2);
+ EXPECT_EQ(t.stakeQdoge(staker, QTREAT_MIN_STAKE), (sint64)QTREAT_MIN_STAKE);
+
+ // All managed QDOGE is accounted for by `staked`, so there is no surplus to release.
+ EXPECT_EQ(t.releaseManagedShares(staker, t.getState()->getQdogeToken(), QTREAT_MIN_STAKE, QTREAT_QX_TRANSFER_FEE), QTREAT_ERR_INVALID_INPUT);
+}
+
+// =============================================================================
+// ASIC mining / drip
+// =============================================================================
+
+TEST(ContractQtreat, LoadAsicPartValidatesInputsAndEnforcesPerRarityBudget)
+{
+ ContractTestingQtreat t;
+ id nonAdmin = getUser(1);
+ increaseEnergy(nonAdmin, 1000);
+
+ EXPECT_EQ(t.loadAsicPart(nonAdmin, 0, 0, 0).returnCode, QTREAT_ERR_ACCESS_DENIED);
+ EXPECT_EQ(t.loadAsicPart(t.adminAddress, 0, 4, 0).returnCode, QTREAT_ERR_INVALID_INPUT); // category >= 4
+ EXPECT_EQ(t.loadAsicPart(t.adminAddress, 0, 0, 5).returnCode, QTREAT_ERR_INVALID_INPUT); // rarity > MAX_RARITY
+
+ EXPECT_EQ(t.loadAsicPart(t.adminAddress, 100, 0, 0).returnCode, QTREAT_OK);
+ EXPECT_EQ(t.loadAsicPart(t.adminAddress, 100, 0, 0).returnCode, QTREAT_ERR_INVALID_INPUT); // duplicate nftId
+
+ // Legendary (rarity 4) budget is only 2 per category.
+ EXPECT_EQ(t.loadAsicPart(t.adminAddress, 200, 1, 4).returnCode, QTREAT_OK);
+ EXPECT_EQ(t.loadAsicPart(t.adminAddress, 201, 1, 4).returnCode, QTREAT_OK);
+ EXPECT_EQ(t.loadAsicPart(t.adminAddress, 202, 1, 4).returnCode, QTREAT_ERR_INVALID_INPUT);
+
+ auto catInfo = t.getAsicCatalogInfo(100);
+ EXPECT_EQ(catInfo.isCataloged, 1u);
+ EXPECT_EQ(catInfo.category, 0u);
+ EXPECT_EQ(catInfo.rarity, 0u);
+ EXPECT_EQ(catInfo.locked, 0u);
+}
+
+TEST(ContractQtreat, RegisterAsicRejectedBeforeCatalogLocked)
+{
+ ContractTestingQtreat t;
+ t.activateQbayMarket();
+ uint32 m = t.mintNft(getUser(1));
+ uint32 c = t.mintNft(getUser(1));
+ uint32 p = t.mintNft(getUser(1));
+ uint32 f = t.mintNft(getUser(1));
+ EXPECT_EQ(t.loadAsicPart(t.adminAddress, m, 0, 0).returnCode, QTREAT_OK);
+ EXPECT_EQ(t.loadAsicPart(t.adminAddress, c, 1, 0).returnCode, QTREAT_OK);
+ EXPECT_EQ(t.loadAsicPart(t.adminAddress, p, 2, 0).returnCode, QTREAT_OK);
+ EXPECT_EQ(t.loadAsicPart(t.adminAddress, f, 3, 0).returnCode, QTREAT_OK);
+
+ EXPECT_EQ(t.registerAsic(getUser(1), m, c, p, f).returnCode, QTREAT_ERR_INVALID_INPUT);
+}
+
+TEST(ContractQtreat, RegisterAndUnregisterAsicAfterCatalogLocked)
+{
+ ContractTestingQtreat t;
+ t.activateQbayMarket();
+
+ id owner = getUser(1);
+ id notOwner = getUser(2);
+ increaseEnergy(notOwner, 1000);
+ uint32 nftMotherboard = t.mintNft(owner); // 0
+ uint32 nftChip = t.mintNft(owner); // 1
+ uint32 nftPsu = t.mintNft(owner); // 2
+ uint32 nftFan = t.mintNft(owner); // 3
+ ASSERT_EQ(nftMotherboard, 0u);
+ ASSERT_EQ(nftFan, 3u);
+
+ t.loadFullAsicCatalog(); // reserves ids 0,1,2,3 as common parts of categories 0..3
+ EXPECT_EQ(t.getState()->getDripStartEpoch(), (uint64)system.epoch);
+
+ // Wrong owner: notOwner doesn't possess these NFTs.
+ EXPECT_EQ(t.registerAsic(notOwner, nftMotherboard, nftChip, nftPsu, nftFan).returnCode, QTREAT_ERR_ACCESS_DENIED);
+
+ auto reg = t.registerAsic(owner, nftMotherboard, nftChip, nftPsu, nftFan);
+ EXPECT_EQ(reg.returnCode, QTREAT_OK);
+ // All 4 parts are COMMON rarity (1 point each) -> weight 4.
+ EXPECT_EQ(reg.asicWeight, 4u);
+ EXPECT_EQ(t.getState()->getTotalAsicCount(), 1u);
+ EXPECT_EQ(t.getState()->getTotalMiningWeight(), 4u);
+
+ auto minerInfo = t.getMinerInfo(owner);
+ EXPECT_EQ(minerInfo.asicCount, 1u);
+ EXPECT_EQ(minerInfo.weight, 4u);
+
+ // Reusing an already-used part is rejected.
+ uint32 spareMotherboard = t.mintNft(owner);
+ // spareMotherboard isn't in the catalog at all yet, so registering with it fails as invalid input too;
+ // instead verify direct re-use of nftMotherboard (already used) is rejected against a fresh valid set:
+ EXPECT_EQ(t.registerAsic(owner, nftMotherboard, nftChip, nftPsu, nftFan).returnCode, QTREAT_ERR_INVALID_INPUT);
+ (void)spareMotherboard;
+
+ // Unregister: non-owner rejected, owner succeeds and frees weight/parts.
+ EXPECT_EQ(t.unregisterAsic(notOwner, reg.rigIndex), QTREAT_ERR_ACCESS_DENIED);
+ EXPECT_EQ(t.unregisterAsic(owner, reg.rigIndex), QTREAT_OK);
+ EXPECT_EQ(t.getState()->getTotalAsicCount(), 0u);
+ EXPECT_EQ(t.getState()->getTotalMiningWeight(), 0u);
+}
+
+TEST(ContractQtreat, MiningRewardSplits5050WithDividendFundAndPaysByWeight)
+{
+ ContractTestingQtreat t;
+ t.activateQbayMarket();
+ id owner = getUser(1);
+ uint32 m = t.mintNft(owner), c = t.mintNft(owner), p = t.mintNft(owner), f = t.mintNft(owner);
+ t.loadFullAsicCatalog();
+ auto reg = t.registerAsic(owner, m, c, p, f);
+ ASSERT_EQ(reg.returnCode, QTREAT_OK);
+
+ EXPECT_EQ(t.setMiningRate(t.adminAddress, QTREAT_MINING_REWARD_MAX + 1), QTREAT_ERR_INVALID_INPUT);
+ EXPECT_EQ(t.setMiningRate(t.adminAddress, 1000000), QTREAT_OK);
+
+ id funder = getUser(9);
+ increaseEnergy(funder, 1000000);
+ EXPECT_EQ(t.depositMiningFund(funder, 1000000), QTREAT_OK);
+
+ // Mining rewards are only ever paid to a rig in the same epoch it's freshly re-verified
+ // (see QTREAT_ASIC_VERIFY_SPREAD_EPOCHS); force the clock to land exactly on this rig's
+ // (slot 0's) next due epoch so no intervening "nobody due" epoch drains miningFund via
+ // the dividend cut first, keeping the expected amounts exact.
+ ASSERT_EQ(reg.rigIndex, 0u);
+ system.epoch = system.epoch - (system.epoch % QTREAT_ASIC_VERIFY_SPREAD_EPOCHS) + QTREAT_ASIC_VERIFY_SPREAD_EPOCHS;
+ ASSERT_EQ((uint64)system.epoch % QTREAT_ASIC_VERIFY_SPREAD_EPOCHS, 0u);
+
+ uint64 dividendBefore = t.getFunds().dividendFund;
+ long long ownerBefore = getBalance(owner);
+ t.advanceEpoch();
+
+ // This epoch's budget slice is rate/SPREAD (see the same reasoning in QTREAT.h), split
+ // 50/50; the sole due rig owns 100% of totalMiningWeight so its scaled-up payment
+ // reconstitutes the full miner half of that slice.
+ uint64 epochSlice = 1000000 / QTREAT_ASIC_VERIFY_SPREAD_EPOCHS;
+ uint64 expectedMinerHalf = (epochSlice / 2) * QTREAT_ASIC_VERIFY_SPREAD_EPOCHS;
+ uint64 expectedDividendHalf = epochSlice - epochSlice / 2;
+ EXPECT_EQ((uint64)(getBalance(owner) - ownerBefore), expectedMinerHalf);
+ EXPECT_EQ(t.getFunds().dividendFund - dividendBefore, expectedDividendHalf);
+}
+
+TEST(ContractQtreat, AsicRigDeactivatedWhenPartOwnershipChanges)
+{
+ ContractTestingQtreat t;
+ t.activateQbayMarket();
+ id owner = getUser(1);
+ id newOwner = getUser(2);
+ uint32 m = t.mintNft(owner), c = t.mintNft(owner), p = t.mintNft(owner), f = t.mintNft(owner);
+ t.loadFullAsicCatalog();
+ auto reg = t.registerAsic(owner, m, c, p, f);
+ ASSERT_EQ(reg.returnCode, QTREAT_OK);
+ ASSERT_EQ(t.getState()->getTotalAsicCount(), 1u);
+
+ t.transferNft(owner, m, newOwner); // motherboard no longer owned by the rig's registered owner
+
+ // Ownership re-verification is spread across QTREAT_ASIC_VERIFY_SPREAD_EPOCHS epochs
+ // (round-robin by rig slot) rather than checking every rig every epoch, so the
+ // deactivation isn't guaranteed on the very next epoch - only within that window.
+ for (uint64 i = 0; i < QTREAT_ASIC_VERIFY_SPREAD_EPOCHS; i++)
+ t.advanceEpoch();
+
+ EXPECT_EQ(t.getState()->getTotalAsicCount(), 0u);
+ EXPECT_EQ(t.getState()->getRig((sint64)reg.rigIndex).active, 0u);
+}
+
+TEST(ContractQtreat, AsicRigStaysActiveUntilItsVerificationSlotComesUp)
+{
+ // The very first registered rig lands at slot 0. constructionEpoch mod
+ // QTREAT_ASIC_VERIFY_SPREAD_EPOCHS != 0 for this fixture's construction epoch, so slot 0
+ // is *not* re-verified on the first END_EPOCH after registration - only after enough
+ // epochs have passed for (slot + epoch) mod SPREAD to hit 0. This pins down the
+ // "eventually consistent, not immediate" tradeoff explicitly rather than leaving it implicit.
+ ContractTestingQtreat t;
+ t.activateQbayMarket();
+ id owner = getUser(1);
+ id newOwner = getUser(2);
+ uint32 m = t.mintNft(owner), c = t.mintNft(owner), p = t.mintNft(owner), f = t.mintNft(owner);
+ t.loadFullAsicCatalog();
+ auto reg = t.registerAsic(owner, m, c, p, f);
+ ASSERT_EQ(reg.returnCode, QTREAT_OK);
+ ASSERT_EQ(reg.rigIndex, 0u);
+ ASSERT_NE(((uint64)reg.rigIndex + (uint64)system.epoch) % QTREAT_ASIC_VERIFY_SPREAD_EPOCHS, 0u)
+ << "test assumption violated: adjust which epoch this fixture starts at, or the rig slot used";
+
+ t.transferNft(owner, m, newOwner);
+ t.advanceEpoch(); // this epoch's slot isn't due for re-verification yet
+
+ EXPECT_EQ(t.getState()->getTotalAsicCount(), 1u);
+ EXPECT_EQ(t.getState()->getRig((sint64)reg.rigIndex).active, 1u);
+}
+
+TEST(ContractQtreat, AsicSoldPartOwnerReceivesNoRewardOnDeactivationEpoch)
+{
+ // Core guarantee: mining reward payment and ownership re-verification always happen
+ // together (same pass, same epoch) - a rig is never paid based on stale ownership. Once
+ // a part is sold away, the old owner collects nothing more, starting with the very epoch
+ // the mismatch is caught (no one-more-payment-then-deactivate window).
+ ContractTestingQtreat t;
+ t.activateQbayMarket();
+ id owner = getUser(1);
+ id newOwner = getUser(2);
+ uint32 m = t.mintNft(owner), c = t.mintNft(owner), p = t.mintNft(owner), f = t.mintNft(owner);
+ t.loadFullAsicCatalog();
+ auto reg = t.registerAsic(owner, m, c, p, f);
+ ASSERT_EQ(reg.returnCode, QTREAT_OK);
+ ASSERT_EQ(reg.rigIndex, 0u);
+
+ EXPECT_EQ(t.setMiningRate(t.adminAddress, 1000000), QTREAT_OK);
+ id funder = getUser(9);
+ increaseEnergy(funder, 1000000);
+ EXPECT_EQ(t.depositMiningFund(funder, 1000000), QTREAT_OK);
+
+ t.transferNft(owner, m, newOwner); // sell before the rig's part ever gets fresh-verified
+
+ // Advance straight to slot 0's next due epoch, same as the payment test, so no
+ // intervening epoch's dividend cut muddies the miningFund balance.
+ system.epoch = system.epoch - (system.epoch % QTREAT_ASIC_VERIFY_SPREAD_EPOCHS) + QTREAT_ASIC_VERIFY_SPREAD_EPOCHS;
+
+ long long ownerBefore = getBalance(owner);
+ t.advanceEpoch();
+
+ // Deactivated (ownership mismatch caught), and the old owner got paid nothing this epoch.
+ EXPECT_EQ(t.getState()->getTotalAsicCount(), 0u);
+ EXPECT_EQ(t.getState()->getRig((sint64)reg.rigIndex).active, 0u);
+ EXPECT_EQ(getBalance(owner), ownerBefore);
+}
+
+TEST(ContractQtreat, DripQdogePaysCatalogedNftHoldersByRarityAndExcludesAdmin)
+{
+ ContractTestingQtreat t;
+ t.activateQbayMarket();
+ id holder = getUser(1);
+ // Mint the 4 NFTs that will land on the reserved common ids (0..3, category==id).
+ uint32 id0 = t.mintNft(holder);
+ uint32 id1 = t.mintNft(holder);
+ uint32 id2 = t.mintNft(holder);
+ uint32 id3 = t.mintNft(holder);
+ ASSERT_EQ(id0, 0u); ASSERT_EQ(id1, 1u); ASSERT_EQ(id2, 2u); ASSERT_EQ(id3, 3u);
+
+ t.loadFullAsicCatalog(); // locks catalog + sets dripStartEpoch; ids 0..3 are COMMON parts
+
+ id nonAdmin = getUser(5);
+ increaseEnergy(nonAdmin, 1000);
+ EXPECT_EQ(t.depositDripQdogeRaw(nonAdmin, 1000), QTREAT_ERR_ACCESS_DENIED);
+
+ EXPECT_EQ(t.depositDripQdogeAsAdmin(1000000), QTREAT_OK);
+
+ // Drip payouts release QDOGE back to QX per-recipient, and that release fee is paid
+ // out of miningFund (not dripQdogePool/stakingFund) - fund it too.
+ id miningFunder = getUser(6);
+ increaseEnergy(miningFunder, 100000);
+ EXPECT_EQ(t.depositMiningFund(miningFunder, 100000), QTREAT_OK);
+
+ sint64 walletBefore = numberOfPossessedShares(QTREAT_QDOGE_ASSETNAME, t.tokenIssuer, holder, holder, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX);
+ t.advanceEpoch();
+ sint64 walletAfter = numberOfPossessedShares(QTREAT_QDOGE_ASSETNAME, t.tokenIssuer, holder, holder, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX);
+
+ // holder possesses 4 COMMON-rarity cataloged NFTs -> 4 * QTREAT_DRIP_QDOGE_COMMON.
+ EXPECT_EQ((uint64)(walletAfter - walletBefore), 4ull * QTREAT_DRIP_QDOGE_COMMON);
+}
diff --git a/test/test.vcxproj b/test/test.vcxproj
index fe0736f6..b87a9846 100644
--- a/test/test.vcxproj
+++ b/test/test.vcxproj
@@ -158,6 +158,7 @@
+
diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters
index 6f776a78..f80463ea 100644
--- a/test/test.vcxproj.filters
+++ b/test/test.vcxproj.filters
@@ -34,6 +34,7 @@
+