diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj
index e26d57a7c..2d73c37e9 100644
--- a/src/Qubic.vcxproj
+++ b/src/Qubic.vcxproj
@@ -28,6 +28,7 @@
+
diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters
index c16c54007..ad488a1d1 100644
--- a/src/Qubic.vcxproj.filters
+++ b/src/Qubic.vcxproj.filters
@@ -305,6 +305,9 @@
contracts
+
+ contracts
+
contract_core
diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h
index 3b97da099..92766716a 100644
--- a/src/contract_core/contract_def.h
+++ b/src/contract_core/contract_def.h
@@ -244,6 +244,16 @@
#define CONTRACT_STATE2_TYPE QDUEL2
#include "contracts/QDuel.h"
+#undef CONTRACT_INDEX
+#undef CONTRACT_STATE_TYPE
+#undef CONTRACT_STATE2_TYPE
+
+#define QUGATE_CONTRACT_INDEX 24
+#define CONTRACT_INDEX QUGATE_CONTRACT_INDEX
+#define CONTRACT_STATE_TYPE QUGATE
+#define CONTRACT_STATE2_TYPE QUGATE2
+#include "contracts/QuGate.h"
+
// new contracts should be added above this line
#ifdef INCLUDE_CONTRACT_TEST_EXAMPLES
@@ -355,6 +365,7 @@ constexpr struct ContractDescription
{"QRP", 199, 10000, sizeof(IPO)}, // proposal in epoch 197, IPO in 198, construction and first use in 199
{"QTF", 199, 10000, sizeof(QTF)}, // proposal in epoch 197, IPO in 198, construction and first use in 199
{"QDUEL", 199, 10000, sizeof(QDUEL)}, // proposal in epoch 197, IPO in 198, construction and first use in 199
+ {"QUGATE", 0, 10000, sizeof(QUGATE)}, // TBD: proposal, IPO, and construction epochs
// new contracts should be added above this line
#ifdef INCLUDE_CONTRACT_TEST_EXAMPLES
{"TESTEXA", 138, 10000, sizeof(TESTEXA)},
@@ -474,6 +485,7 @@ static void initializeContracts()
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRP);
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QTF);
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QDUEL);
+ REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QUGATE);
// new contracts should be added above this line
#ifdef INCLUDE_CONTRACT_TEST_EXAMPLES
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA);
diff --git a/src/contracts/QuGate.h b/src/contracts/QuGate.h
new file mode 100644
index 000000000..f614bbf42
--- /dev/null
+++ b/src/contracts/QuGate.h
@@ -0,0 +1,1244 @@
+// QuGate.h - Programmable Payment Gate Contract
+// Short name: QUGATE
+// Description: Universal payment routing with predefined gate modes.
+// Create gates with configurable rules, point payments at them,
+// and QU flows according to the logic.
+//
+// Gate Modes:
+// SPLIT - Distribute to N addresses by ratio (e.g. 40/30/20/10)
+// ROUND_ROBIN - Cycle through addresses, one per payment
+// THRESHOLD - Accumulate until amount reached, then forward
+// RANDOM - Select one recipient per payment using tick-based entropy
+// CONDITIONAL - Only forward if sender matches whitelist, else bounce
+//
+// Anti-Spam:
+// - Escalating creation fee: cost increases as capacity fills
+// - Gate expiry: inactive gates auto-close after N epochs
+// - Dust burn: sends below minimum are burned
+// - All fees are deflationary (burned, not accumulated)
+
+using namespace QPI;
+
+// Capacity scales with network via X_MULTIPLIER
+constexpr uint64 QUGATE_INITIAL_MAX_GATES = 4096;
+constexpr uint64 QUGATE_MAX_GATES = QUGATE_INITIAL_MAX_GATES * X_MULTIPLIER;
+constexpr uint64 QUGATE_MAX_RECIPIENTS = 8;
+constexpr uint64 QUGATE_MAX_RATIO = 10000; // Max ratio per recipient (prevents overflow)
+
+// Default fees — initial values, changeable via shareholder vote
+constexpr uint64 QUGATE_DEFAULT_CREATION_FEE = 100000;
+constexpr uint64 QUGATE_DEFAULT_MIN_SEND = 1000;
+
+// Escalating fee: fee = baseFee * (1 + activeGates / FEE_ESCALATION_STEP)
+constexpr uint64 QUGATE_FEE_ESCALATION_STEP = 1024;
+
+// Gate expiry: gates with no activity for this many epochs auto-close
+constexpr uint64 QUGATE_DEFAULT_EXPIRY_EPOCHS = 50;
+
+// Gate modes
+constexpr uint8 QUGATE_MODE_SPLIT = 0;
+constexpr uint8 QUGATE_MODE_ROUND_ROBIN = 1;
+constexpr uint8 QUGATE_MODE_THRESHOLD = 2;
+constexpr uint8 QUGATE_MODE_RANDOM = 3;
+constexpr uint8 QUGATE_MODE_CONDITIONAL = 4;
+
+// Status codes — used in procedure outputs and logger _type
+constexpr sint64 QUGATE_SUCCESS = 0;
+constexpr sint64 QUGATE_INVALID_GATE_ID = -1;
+constexpr sint64 QUGATE_GATE_NOT_ACTIVE = -2;
+constexpr sint64 QUGATE_UNAUTHORIZED = -3;
+constexpr sint64 QUGATE_INVALID_MODE = -4;
+constexpr sint64 QUGATE_INVALID_RECIPIENT_COUNT = -5;
+constexpr sint64 QUGATE_INVALID_RATIO = -6;
+constexpr sint64 QUGATE_INSUFFICIENT_FEE = -7;
+constexpr sint64 QUGATE_NO_FREE_SLOTS = -8;
+constexpr sint64 QUGATE_DUST_AMOUNT = -9;
+constexpr sint64 QUGATE_INVALID_THRESHOLD = -10;
+constexpr sint64 QUGATE_INVALID_SENDER_COUNT = -11;
+constexpr sint64 QUGATE_CONDITIONAL_REJECTED = -12;
+
+// Log type constants (positive = success events, high numbers = actions)
+constexpr uint32 QUGATE_LOG_GATE_CREATED = 1;
+constexpr uint32 QUGATE_LOG_GATE_CLOSED = 2;
+constexpr uint32 QUGATE_LOG_GATE_UPDATED = 3;
+constexpr uint32 QUGATE_LOG_PAYMENT_FORWARDED = 4;
+constexpr uint32 QUGATE_LOG_PAYMENT_BOUNCED = 5;
+constexpr uint32 QUGATE_LOG_DUST_BURNED = 6;
+constexpr uint32 QUGATE_LOG_FEE_CHANGED = 7;
+constexpr uint32 QUGATE_LOG_GATE_EXPIRED = 8; //
+// Failure log types use high range
+constexpr uint32 QUGATE_LOG_FAIL_INVALID_GATE = 100;
+constexpr uint32 QUGATE_LOG_FAIL_NOT_ACTIVE = 101;
+constexpr uint32 QUGATE_LOG_FAIL_UNAUTHORIZED = 102;
+constexpr uint32 QUGATE_LOG_FAIL_INVALID_PARAMS = 103;
+constexpr uint32 QUGATE_LOG_FAIL_INSUFFICIENT_FEE = 104;
+constexpr uint32 QUGATE_LOG_FAIL_NO_SLOTS = 105;
+
+// Asset name for shareholder proposals
+constexpr uint64 QUGATE_CONTRACT_ASSET_NAME = 76228174763345ULL; // "QUGATE" as uint64 little-endian
+
+// Future extension struct (Qubic convention)
+struct QUGATE2
+{
+};
+
+struct QUGATE : public ContractBase
+{
+public:
+ // =============================================
+ // Logging structs
+ // =============================================
+
+ struct QuGateLogger
+ {
+ uint32 _contractIndex;
+ uint32 _type; // maps to QUGATE_LOG_* constants
+ uint64 gateId;
+ id sender;
+ sint64 amount;
+ sint8 _terminator; // Qubic logs data before this field only
+ };
+
+ // =============================================
+ // Data structures for gate configuration
+ // =============================================
+
+ struct GateConfig
+ {
+ id owner;
+ uint8 mode;
+ uint8 recipientCount;
+ uint8 active;
+ uint8 allowedSenderCount;
+ uint16 createdEpoch; // epoch() returns uint16
+ uint16 lastActivityEpoch; // updated on create/send/update
+ uint64 totalReceived;
+ uint64 totalForwarded;
+ uint64 currentBalance;
+ uint64 threshold;
+ uint64 roundRobinIndex;
+ Array recipients;
+ Array ratios;
+ Array allowedSenders;
+ };
+
+ // =============================================
+ // Procedure inputs/outputs
+ // =============================================
+
+ struct createGate_input
+ {
+ uint8 mode;
+ uint8 recipientCount;
+ Array recipients;
+ Array ratios;
+ uint64 threshold;
+ Array allowedSenders;
+ uint8 allowedSenderCount;
+ };
+ struct createGate_output
+ {
+ sint64 status; //
+ uint64 gateId;
+ uint64 feePaid; // actual fee charged (for transparency)
+ };
+
+ struct sendToGate_input
+ {
+ uint64 gateId;
+ };
+ struct sendToGate_output
+ {
+ sint64 status; //
+ };
+
+ struct closeGate_input
+ {
+ uint64 gateId;
+ };
+ struct closeGate_output
+ {
+ sint64 status; //
+ };
+
+ struct updateGate_input
+ {
+ uint64 gateId;
+ uint8 recipientCount;
+ Array recipients;
+ Array ratios;
+ uint64 threshold;
+ Array allowedSenders;
+ uint8 allowedSenderCount;
+ };
+ struct updateGate_output
+ {
+ sint64 status; //
+ };
+
+ // =============================================
+ // Function inputs/outputs (read-only queries)
+ // =============================================
+
+ struct getGate_input
+ {
+ uint64 gateId;
+ };
+ struct getGate_output
+ {
+ uint8 mode;
+ uint8 recipientCount;
+ uint8 active;
+ id owner;
+ uint64 totalReceived;
+ uint64 totalForwarded;
+ uint64 currentBalance;
+ uint64 threshold;
+ uint16 createdEpoch; //
+ uint16 lastActivityEpoch; //
+ Array recipients;
+ Array ratios;
+ };
+
+ struct getGateCount_input
+ {
+ };
+ struct getGateCount_output
+ {
+ uint64 totalGates;
+ uint64 activeGates;
+ uint64 totalBurned; //
+ };
+
+ struct getGatesByOwner_input
+ {
+ id owner;
+ };
+ struct getGatesByOwner_output
+ {
+ Array gateIds;
+ uint64 count;
+ };
+
+ // Batch gate query
+ struct getGateBatch_input
+ {
+ Array gateIds;
+ };
+ struct getGateBatch_output
+ {
+ Array gates;
+ };
+
+ // Fee query — includes current escalated fee and expiry setting
+ struct getFees_input
+ {
+ };
+ struct getFees_output
+ {
+ uint64 creationFee; // base fee
+ uint64 currentCreationFee; // actual fee right now (after escalation)
+ uint64 minSendAmount;
+ uint64 expiryEpochs; //
+ };
+
+protected:
+ // =============================================
+ // Contract state
+ // =============================================
+
+ uint64 _gateCount;
+ uint64 _activeGates;
+ uint64 _totalBurned; // cumulative QU burned
+ Array _gates;
+
+ // Free-list for slot reuse
+ Array _freeSlots;
+ uint64 _freeCount;
+
+ // Shareholder-adjustable parameters
+ uint64 _creationFee; // base creation fee
+ uint64 _minSendAmount;
+ uint64 _expiryEpochs; // epochs of inactivity before auto-close
+
+ // Shareholder proposal storage
+ // DEFINE_SHAREHOLDER_PROPOSAL_STORAGE(4, QUGATE_CONTRACT_ASSET_NAME);
+ // NOTE: Uncomment when QUGATE_CONTRACT_ASSET_NAME is set at registration time
+ // For now, fees are set in INITIALIZE and cannot be changed until shareholder infra is wired.
+
+ // =============================================
+ // Locals — all variables declared here, not inline
+ // =============================================
+
+ struct createGate_locals
+ {
+ QuGateLogger logger;
+ GateConfig newGate;
+ uint64 totalRatio;
+ uint64 i;
+ uint64 slotIdx;
+ uint64 currentFee; // escalated fee
+ };
+
+ struct processSplit_input
+ {
+ uint64 gateIdx;
+ sint64 amount;
+ };
+ struct processSplit_output
+ {
+ uint64 forwarded;
+ };
+ struct processSplit_locals
+ {
+ GateConfig gate;
+ uint64 totalRatio;
+ uint64 share;
+ uint64 distributed;
+ uint64 i;
+ };
+
+ struct processRoundRobin_input
+ {
+ uint64 gateIdx;
+ sint64 amount;
+ };
+ struct processRoundRobin_output
+ {
+ uint64 forwarded;
+ };
+ struct processRoundRobin_locals
+ {
+ GateConfig gate;
+ };
+
+ struct processThreshold_input
+ {
+ uint64 gateIdx;
+ sint64 amount;
+ };
+ struct processThreshold_output
+ {
+ uint64 forwarded;
+ };
+ struct processThreshold_locals
+ {
+ GateConfig gate;
+ };
+
+ struct processRandom_input
+ {
+ uint64 gateIdx;
+ sint64 amount;
+ };
+ struct processRandom_output
+ {
+ uint64 forwarded;
+ };
+ struct processRandom_locals
+ {
+ GateConfig gate;
+ uint64 recipientIdx;
+ };
+
+ struct processConditional_input
+ {
+ uint64 gateIdx;
+ sint64 amount;
+ };
+ struct processConditional_output
+ {
+ sint64 status;
+ uint64 forwarded;
+ };
+ struct processConditional_locals
+ {
+ GateConfig gate;
+ uint64 i;
+ uint8 senderAllowed;
+ };
+
+
+ struct sendToGate_locals
+ {
+ QuGateLogger logger;
+ GateConfig gate;
+ sint64 amount;
+ uint64 idx;
+ processSplit_input splitIn;
+ processSplit_output splitOut;
+ processSplit_locals splitLocals;
+ processRoundRobin_input rrIn;
+ processRoundRobin_output rrOut;
+ processRoundRobin_locals rrLocals;
+ processThreshold_input threshIn;
+ processThreshold_output threshOut;
+ processThreshold_locals threshLocals;
+ processRandom_input randIn;
+ processRandom_output randOut;
+ processRandom_locals randLocals;
+ processConditional_input condIn;
+ processConditional_output condOut;
+ processConditional_locals condLocals;
+ };
+
+ struct closeGate_locals
+ {
+ QuGateLogger logger;
+ GateConfig gate;
+ };
+
+ struct updateGate_locals
+ {
+ QuGateLogger logger;
+ GateConfig gate;
+ uint64 totalRatio;
+ uint64 i;
+ };
+
+ struct getGate_locals
+ {
+ GateConfig gate;
+ uint64 i;
+ };
+
+ struct getGateBatch_locals //
+ {
+ uint64 i;
+ uint64 j;
+ GateConfig gate;
+ getGate_output entry;
+ };
+
+ struct getGatesByOwner_locals
+ {
+ uint64 i;
+ };
+
+ struct END_EPOCH_locals //
+ {
+ uint64 i;
+ QuGateLogger logger;
+ GateConfig gate;
+ };
+
+ // =============================================
+ // Procedures
+ // =============================================
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(createGate)
+ {
+ output.status = QUGATE_SUCCESS;
+ output.gateId = 0;
+ output.feePaid = 0;
+
+ // Init logger
+ locals.logger._contractIndex = CONTRACT_INDEX;
+ locals.logger.sender = qpi.invocator();
+ locals.logger.gateId = 0;
+ locals.logger.amount = qpi.invocationReward();
+
+ // Calculate escalated fee: baseFee * (1 + activeGates / STEP)
+ locals.currentFee = state._creationFee * (1 + QPI::div(state._activeGates, QUGATE_FEE_ESCALATION_STEP));
+
+ // Validate creation fee (escalated)
+ if (qpi.invocationReward() < (sint64)locals.currentFee)
+ {
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+ output.status = QUGATE_INSUFFICIENT_FEE;
+ locals.logger._type = QUGATE_LOG_FAIL_INSUFFICIENT_FEE;
+ LOG_INFO(locals.logger);
+ return;
+ }
+
+ // Validate mode
+ if (input.mode > QUGATE_MODE_CONDITIONAL)
+ {
+ // Refund all
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_INVALID_MODE;
+ locals.logger._type = QUGATE_LOG_FAIL_INVALID_PARAMS;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+
+ // Validate recipient count
+ if (input.recipientCount == 0 || input.recipientCount > QUGATE_MAX_RECIPIENTS)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_INVALID_RECIPIENT_COUNT;
+ locals.logger._type = QUGATE_LOG_FAIL_INVALID_PARAMS;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+
+ // Check capacity — try free-list first
+ if (state._freeCount == 0 && state._gateCount >= QUGATE_MAX_GATES)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_NO_FREE_SLOTS;
+ locals.logger._type = QUGATE_LOG_FAIL_NO_SLOTS;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+
+ // Validate SPLIT ratios
+ if (input.mode == QUGATE_MODE_SPLIT)
+ {
+ locals.totalRatio = 0;
+ for (locals.i = 0; locals.i < input.recipientCount; locals.i++)
+ {
+ if (input.ratios.get(locals.i) > QUGATE_MAX_RATIO)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_INVALID_RATIO;
+ locals.logger._type = QUGATE_LOG_FAIL_INVALID_PARAMS;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+ locals.totalRatio += input.ratios.get(locals.i);
+ }
+ if (locals.totalRatio == 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_INVALID_RATIO;
+ locals.logger._type = QUGATE_LOG_FAIL_INVALID_PARAMS;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+ }
+
+ // Validate THRESHOLD > 0
+ if (input.mode == QUGATE_MODE_THRESHOLD && input.threshold == 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_INVALID_THRESHOLD;
+ locals.logger._type = QUGATE_LOG_FAIL_INVALID_PARAMS;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+
+ // Validate allowedSenderCount
+ if (input.allowedSenderCount > QUGATE_MAX_RECIPIENTS)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_INVALID_SENDER_COUNT;
+ locals.logger._type = QUGATE_LOG_FAIL_INVALID_PARAMS;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+
+ // Build the gate config
+ locals.newGate.owner = qpi.invocator();
+ locals.newGate.mode = input.mode;
+ locals.newGate.recipientCount = input.recipientCount;
+ locals.newGate.active = 1;
+ locals.newGate.allowedSenderCount = input.allowedSenderCount;
+ locals.newGate.createdEpoch = qpi.epoch(); // uint16
+ locals.newGate.lastActivityEpoch = qpi.epoch(); //
+ locals.newGate.totalReceived = 0;
+ locals.newGate.totalForwarded = 0;
+ locals.newGate.currentBalance = 0;
+ locals.newGate.threshold = input.threshold;
+ locals.newGate.roundRobinIndex = 0;
+
+ for (locals.i = 0; locals.i < QUGATE_MAX_RECIPIENTS; locals.i++)
+ {
+ if (locals.i < input.recipientCount)
+ {
+ locals.newGate.recipients.set(locals.i, input.recipients.get(locals.i));
+ locals.newGate.ratios.set(locals.i, input.ratios.get(locals.i));
+ }
+ else
+ {
+ locals.newGate.recipients.set(locals.i, id::zero());
+ locals.newGate.ratios.set(locals.i, 0);
+ }
+ }
+
+ for (locals.i = 0; locals.i < QUGATE_MAX_RECIPIENTS; locals.i++)
+ {
+ if (locals.i < input.allowedSenderCount)
+ {
+ locals.newGate.allowedSenders.set(locals.i, input.allowedSenders.get(locals.i));
+ }
+ else
+ {
+ locals.newGate.allowedSenders.set(locals.i, id::zero());
+ }
+ }
+
+ // Allocate slot — free-list first, then fresh
+ if (state._freeCount > 0)
+ {
+ state._freeCount -= 1;
+ locals.slotIdx = state._freeSlots.get(state._freeCount);
+ }
+ else
+ {
+ locals.slotIdx = state._gateCount;
+ state._gateCount += 1;
+ }
+
+ state._gates.set(locals.slotIdx, locals.newGate);
+ output.gateId = locals.slotIdx + 1; // 1-indexed for users
+ state._activeGates += 1;
+
+ // Burn the escalated creation fee
+ qpi.burn(locals.currentFee);
+ state._totalBurned += locals.currentFee; //
+ output.feePaid = locals.currentFee; //
+
+ // Refund any excess
+ if (qpi.invocationReward() > (sint64)locals.currentFee)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)locals.currentFee);
+ }
+
+ output.status = QUGATE_SUCCESS;
+
+ // Log success
+ locals.logger._type = QUGATE_LOG_GATE_CREATED;
+ locals.logger.gateId = output.gateId;
+ LOG_INFO(locals.logger);
+ }
+
+ // =============================================
+ // Private mode processors
+ // =============================================
+
+ PRIVATE_PROCEDURE_WITH_LOCALS(processSplit)
+ {
+ locals.gate = state._gates.get(input.gateIdx);
+
+ locals.totalRatio = 0;
+ for (locals.i = 0; locals.i < locals.gate.recipientCount; locals.i++)
+ {
+ locals.totalRatio += locals.gate.ratios.get(locals.i);
+ }
+
+ locals.distributed = 0;
+ for (locals.i = 0; locals.i < locals.gate.recipientCount; locals.i++)
+ {
+ if (locals.i == locals.gate.recipientCount - 1)
+ {
+ locals.share = input.amount - locals.distributed;
+ }
+ else
+ {
+ locals.share = QPI::div((uint64)input.amount, locals.totalRatio) * locals.gate.ratios.get(locals.i)
+ + QPI::div(QPI::mod((uint64)input.amount, locals.totalRatio) * locals.gate.ratios.get(locals.i), locals.totalRatio);
+ }
+
+ if (locals.share > 0)
+ {
+ qpi.transfer(locals.gate.recipients.get(locals.i), locals.share);
+ locals.distributed += locals.share;
+ }
+ }
+
+ locals.gate.totalForwarded += locals.distributed;
+ state._gates.set(input.gateIdx, locals.gate);
+ output.forwarded = locals.distributed;
+ }
+
+ PRIVATE_PROCEDURE_WITH_LOCALS(processRoundRobin)
+ {
+ locals.gate = state._gates.get(input.gateIdx);
+
+ qpi.transfer(locals.gate.recipients.get(locals.gate.roundRobinIndex), input.amount);
+ locals.gate.totalForwarded += input.amount;
+ locals.gate.roundRobinIndex = QPI::mod(locals.gate.roundRobinIndex + 1, (uint64)locals.gate.recipientCount);
+
+ state._gates.set(input.gateIdx, locals.gate);
+ output.forwarded = input.amount;
+ }
+
+ PRIVATE_PROCEDURE_WITH_LOCALS(processThreshold)
+ {
+ locals.gate = state._gates.get(input.gateIdx);
+
+ locals.gate.currentBalance += input.amount;
+ output.forwarded = 0;
+
+ if (locals.gate.currentBalance >= locals.gate.threshold)
+ {
+ qpi.transfer(locals.gate.recipients.get(0), locals.gate.currentBalance);
+ output.forwarded = locals.gate.currentBalance;
+ locals.gate.totalForwarded += locals.gate.currentBalance;
+ locals.gate.currentBalance = 0;
+ }
+
+ state._gates.set(input.gateIdx, locals.gate);
+ }
+
+ PRIVATE_PROCEDURE_WITH_LOCALS(processRandom)
+ {
+ locals.gate = state._gates.get(input.gateIdx);
+
+ locals.recipientIdx = QPI::mod(locals.gate.totalReceived + qpi.tick(), (uint64)locals.gate.recipientCount);
+ qpi.transfer(locals.gate.recipients.get(locals.recipientIdx), input.amount);
+ locals.gate.totalForwarded += input.amount;
+
+ state._gates.set(input.gateIdx, locals.gate);
+ output.forwarded = input.amount;
+ }
+
+ PRIVATE_PROCEDURE_WITH_LOCALS(processConditional)
+ {
+ locals.gate = state._gates.get(input.gateIdx);
+ output.status = QUGATE_SUCCESS;
+ output.forwarded = 0;
+
+ locals.senderAllowed = 0;
+ for (locals.i = 0; locals.i < locals.gate.allowedSenderCount; locals.i++)
+ {
+ if (locals.senderAllowed == 0 && locals.gate.allowedSenders.get(locals.i) == qpi.invocator())
+ {
+ locals.senderAllowed = 1;
+ }
+ }
+
+ if (locals.senderAllowed)
+ {
+ qpi.transfer(locals.gate.recipients.get(0), input.amount);
+ locals.gate.totalForwarded += input.amount;
+ output.forwarded = input.amount;
+ }
+ else
+ {
+ qpi.transfer(qpi.invocator(), input.amount);
+ output.status = QUGATE_CONDITIONAL_REJECTED;
+ }
+
+ state._gates.set(input.gateIdx, locals.gate);
+ }
+
+ // =============================================
+ // Procedures
+ // =============================================
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(sendToGate)
+ {
+ output.status = QUGATE_SUCCESS;
+
+ locals.logger._contractIndex = CONTRACT_INDEX;
+ locals.logger.sender = qpi.invocator();
+ locals.logger.amount = qpi.invocationReward();
+ locals.logger.gateId = input.gateId;
+
+ if (input.gateId == 0 || input.gateId > state._gateCount)
+ {
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+ output.status = QUGATE_INVALID_GATE_ID;
+ locals.logger._type = QUGATE_LOG_FAIL_INVALID_GATE;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+
+ locals.idx = input.gateId - 1;
+ locals.gate = state._gates.get(locals.idx);
+
+ if (locals.gate.active == 0)
+ {
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+ output.status = QUGATE_GATE_NOT_ACTIVE;
+ locals.logger._type = QUGATE_LOG_FAIL_NOT_ACTIVE;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+
+ locals.amount = qpi.invocationReward();
+ if (locals.amount <= 0)
+ {
+ output.status = QUGATE_DUST_AMOUNT;
+ return;
+ }
+
+ if (locals.amount < (sint64)state._minSendAmount)
+ {
+ qpi.burn(locals.amount);
+ state._totalBurned += locals.amount;
+ output.status = QUGATE_DUST_AMOUNT;
+ locals.logger._type = QUGATE_LOG_DUST_BURNED;
+ LOG_INFO(locals.logger);
+ return;
+ }
+
+ // Update activity and track received
+ locals.gate.lastActivityEpoch = qpi.epoch();
+ locals.gate.totalReceived += locals.amount;
+ state._gates.set(locals.idx, locals.gate);
+
+ // Dispatch to mode-specific handler
+ if (locals.gate.mode == QUGATE_MODE_SPLIT)
+ {
+ locals.splitIn.gateIdx = locals.idx;
+ locals.splitIn.amount = locals.amount;
+ processSplit(qpi, state, locals.splitIn, locals.splitOut, locals.splitLocals);
+ locals.logger._type = QUGATE_LOG_PAYMENT_FORWARDED;
+ LOG_INFO(locals.logger);
+ }
+ else if (locals.gate.mode == QUGATE_MODE_ROUND_ROBIN)
+ {
+ locals.rrIn.gateIdx = locals.idx;
+ locals.rrIn.amount = locals.amount;
+ processRoundRobin(qpi, state, locals.rrIn, locals.rrOut, locals.rrLocals);
+ locals.logger._type = QUGATE_LOG_PAYMENT_FORWARDED;
+ LOG_INFO(locals.logger);
+ }
+ else if (locals.gate.mode == QUGATE_MODE_THRESHOLD)
+ {
+ locals.threshIn.gateIdx = locals.idx;
+ locals.threshIn.amount = locals.amount;
+ processThreshold(qpi, state, locals.threshIn, locals.threshOut, locals.threshLocals);
+ if (locals.threshOut.forwarded > 0)
+ {
+ locals.logger._type = QUGATE_LOG_PAYMENT_FORWARDED;
+ LOG_INFO(locals.logger);
+ }
+ }
+ else if (locals.gate.mode == QUGATE_MODE_RANDOM)
+ {
+ locals.randIn.gateIdx = locals.idx;
+ locals.randIn.amount = locals.amount;
+ processRandom(qpi, state, locals.randIn, locals.randOut, locals.randLocals);
+ locals.logger._type = QUGATE_LOG_PAYMENT_FORWARDED;
+ LOG_INFO(locals.logger);
+ }
+ else if (locals.gate.mode == QUGATE_MODE_CONDITIONAL)
+ {
+ locals.condIn.gateIdx = locals.idx;
+ locals.condIn.amount = locals.amount;
+ processConditional(qpi, state, locals.condIn, locals.condOut, locals.condLocals);
+ if (locals.condOut.status == QUGATE_SUCCESS)
+ {
+ locals.logger._type = QUGATE_LOG_PAYMENT_FORWARDED;
+ LOG_INFO(locals.logger);
+ }
+ else
+ {
+ output.status = locals.condOut.status;
+ locals.logger._type = QUGATE_LOG_PAYMENT_BOUNCED;
+ LOG_INFO(locals.logger);
+ }
+ }
+ }
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(closeGate)
+ {
+ output.status = QUGATE_SUCCESS;
+
+ // Init logger
+ locals.logger._contractIndex = CONTRACT_INDEX;
+ locals.logger.sender = qpi.invocator();
+ locals.logger.gateId = input.gateId;
+ locals.logger.amount = 0;
+
+ if (input.gateId == 0 || input.gateId > state._gateCount)
+ {
+ if (qpi.invocationReward() > 0)
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_INVALID_GATE_ID;
+ locals.logger._type = QUGATE_LOG_FAIL_INVALID_GATE;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+
+ locals.gate = state._gates.get(input.gateId - 1);
+
+ if (locals.gate.owner != qpi.invocator())
+ {
+ if (qpi.invocationReward() > 0)
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_UNAUTHORIZED;
+ locals.logger._type = QUGATE_LOG_FAIL_UNAUTHORIZED;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+
+ if (locals.gate.active == 0)
+ {
+ if (qpi.invocationReward() > 0)
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_GATE_NOT_ACTIVE;
+ locals.logger._type = QUGATE_LOG_FAIL_NOT_ACTIVE;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+
+ // Refund any held balance (THRESHOLD mode)
+ if (locals.gate.currentBalance > 0)
+ {
+ qpi.transfer(locals.gate.owner, locals.gate.currentBalance);
+ locals.gate.currentBalance = 0;
+ }
+
+ locals.gate.active = 0;
+ state._gates.set(input.gateId - 1, locals.gate);
+ state._activeGates -= 1;
+
+ // Push slot onto free-list for reuse
+ state._freeSlots.set(state._freeCount, input.gateId - 1);
+ state._freeCount += 1;
+
+ // Refund invocation reward
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+
+ // Log success
+ locals.logger._type = QUGATE_LOG_GATE_CLOSED;
+ LOG_INFO(locals.logger);
+ }
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(updateGate)
+ {
+ output.status = QUGATE_SUCCESS;
+
+ // Init logger
+ locals.logger._contractIndex = CONTRACT_INDEX;
+ locals.logger.sender = qpi.invocator();
+ locals.logger.gateId = input.gateId;
+ locals.logger.amount = 0;
+
+ if (input.gateId == 0 || input.gateId > state._gateCount)
+ {
+ if (qpi.invocationReward() > 0)
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_INVALID_GATE_ID;
+ locals.logger._type = QUGATE_LOG_FAIL_INVALID_GATE;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+
+ locals.gate = state._gates.get(input.gateId - 1);
+
+ if (locals.gate.owner != qpi.invocator())
+ {
+ if (qpi.invocationReward() > 0)
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_UNAUTHORIZED;
+ locals.logger._type = QUGATE_LOG_FAIL_UNAUTHORIZED;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+
+ if (locals.gate.active == 0)
+ {
+ if (qpi.invocationReward() > 0)
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_GATE_NOT_ACTIVE;
+ locals.logger._type = QUGATE_LOG_FAIL_NOT_ACTIVE;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+
+ // Validate new recipient count
+ if (input.recipientCount == 0 || input.recipientCount > QUGATE_MAX_RECIPIENTS)
+ {
+ if (qpi.invocationReward() > 0)
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_INVALID_RECIPIENT_COUNT;
+ locals.logger._type = QUGATE_LOG_FAIL_INVALID_PARAMS;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+
+ // Validate allowedSenderCount bounds
+ if (input.allowedSenderCount > QUGATE_MAX_RECIPIENTS)
+ {
+ if (qpi.invocationReward() > 0)
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_INVALID_SENDER_COUNT;
+ locals.logger._type = QUGATE_LOG_FAIL_INVALID_PARAMS;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+
+ // Validate SPLIT ratios if gate is SPLIT mode
+ if (locals.gate.mode == QUGATE_MODE_SPLIT)
+ {
+ locals.totalRatio = 0;
+ for (locals.i = 0; locals.i < input.recipientCount; locals.i++)
+ {
+ if (input.ratios.get(locals.i) > QUGATE_MAX_RATIO)
+ {
+ if (qpi.invocationReward() > 0)
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_INVALID_RATIO;
+ locals.logger._type = QUGATE_LOG_FAIL_INVALID_PARAMS;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+ locals.totalRatio += input.ratios.get(locals.i);
+ }
+ if (locals.totalRatio == 0)
+ {
+ if (qpi.invocationReward() > 0)
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_INVALID_RATIO;
+ locals.logger._type = QUGATE_LOG_FAIL_INVALID_PARAMS;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+ }
+
+ // Validate THRESHOLD if gate is THRESHOLD mode
+ if (locals.gate.mode == QUGATE_MODE_THRESHOLD && input.threshold == 0)
+ {
+ if (qpi.invocationReward() > 0)
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ output.status = QUGATE_INVALID_THRESHOLD;
+ locals.logger._type = QUGATE_LOG_FAIL_INVALID_PARAMS;
+ LOG_WARNING(locals.logger);
+ return;
+ }
+
+ // Update last activity epoch
+ locals.gate.lastActivityEpoch = qpi.epoch();
+
+ // Update configuration (mode stays the same)
+ locals.gate.recipientCount = input.recipientCount;
+ locals.gate.threshold = input.threshold;
+ locals.gate.allowedSenderCount = input.allowedSenderCount;
+
+ // Use locals.i, zero stale slots
+ for (locals.i = 0; locals.i < QUGATE_MAX_RECIPIENTS; locals.i++)
+ {
+ if (locals.i < input.recipientCount)
+ {
+ locals.gate.recipients.set(locals.i, input.recipients.get(locals.i));
+ locals.gate.ratios.set(locals.i, input.ratios.get(locals.i));
+ }
+ else
+ {
+ locals.gate.recipients.set(locals.i, id::zero());
+ locals.gate.ratios.set(locals.i, 0);
+ }
+
+ if (locals.i < input.allowedSenderCount)
+ {
+ locals.gate.allowedSenders.set(locals.i, input.allowedSenders.get(locals.i));
+ }
+ else
+ {
+ locals.gate.allowedSenders.set(locals.i, id::zero());
+ }
+ }
+
+ state._gates.set(input.gateId - 1, locals.gate);
+
+ if (qpi.invocationReward() > 0)
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+
+ // Log success
+ locals.logger._type = QUGATE_LOG_GATE_UPDATED;
+ LOG_INFO(locals.logger);
+ }
+
+ // =============================================
+ // Functions (read-only)
+ // =============================================
+
+ PUBLIC_FUNCTION_WITH_LOCALS(getGate)
+ {
+ if (input.gateId == 0 || input.gateId > state._gateCount)
+ {
+ output.active = 0;
+ return;
+ }
+
+ locals.gate = state._gates.get(input.gateId - 1);
+ output.mode = locals.gate.mode;
+ output.recipientCount = locals.gate.recipientCount;
+ output.active = locals.gate.active;
+ output.owner = locals.gate.owner;
+ output.totalReceived = locals.gate.totalReceived;
+ output.totalForwarded = locals.gate.totalForwarded;
+ output.currentBalance = locals.gate.currentBalance;
+ output.threshold = locals.gate.threshold;
+ output.createdEpoch = locals.gate.createdEpoch;
+ output.lastActivityEpoch = locals.gate.lastActivityEpoch; //
+
+ for (locals.i = 0; locals.i < QUGATE_MAX_RECIPIENTS; locals.i++)
+ {
+ output.recipients.set(locals.i, locals.gate.recipients.get(locals.i));
+ output.ratios.set(locals.i, locals.gate.ratios.get(locals.i));
+ }
+ }
+
+ PUBLIC_FUNCTION(getGateCount)
+ {
+ output.totalGates = state._gateCount;
+ output.activeGates = state._activeGates;
+ output.totalBurned = state._totalBurned; //
+ }
+
+ PUBLIC_FUNCTION_WITH_LOCALS(getGatesByOwner)
+ {
+ output.count = 0;
+ for (locals.i = 0; locals.i < state._gateCount && output.count < 16; locals.i++)
+ {
+ if (state._gates.get(locals.i).owner == input.owner)
+ {
+ output.gateIds.set(output.count, locals.i + 1);
+ output.count += 1;
+ }
+ }
+ }
+
+ // Batch gate query — fetch up to 32 gates in one call
+ PUBLIC_FUNCTION_WITH_LOCALS(getGateBatch)
+ {
+ for (locals.i = 0; locals.i < 32; locals.i++)
+ {
+ if (input.gateIds.get(locals.i) == 0 || input.gateIds.get(locals.i) > state._gateCount)
+ {
+ // Zero the entry for invalid IDs — clear all fields to avoid stale data
+ locals.entry.mode = 0;
+ locals.entry.recipientCount = 0;
+ locals.entry.active = 0;
+ locals.entry.owner = id::zero();
+ locals.entry.totalReceived = 0;
+ locals.entry.totalForwarded = 0;
+ locals.entry.currentBalance = 0;
+ locals.entry.threshold = 0;
+ locals.entry.createdEpoch = 0;
+ locals.entry.lastActivityEpoch = 0;
+ for (locals.j = 0; locals.j < QUGATE_MAX_RECIPIENTS; locals.j++)
+ {
+ locals.entry.recipients.set(locals.j, id::zero());
+ locals.entry.ratios.set(locals.j, 0);
+ }
+ output.gates.set(locals.i, locals.entry);
+ }
+ else
+ {
+ locals.gate = state._gates.get(input.gateIds.get(locals.i) - 1);
+
+ locals.entry.mode = locals.gate.mode;
+ locals.entry.recipientCount = locals.gate.recipientCount;
+ locals.entry.active = locals.gate.active;
+ locals.entry.owner = locals.gate.owner;
+ locals.entry.totalReceived = locals.gate.totalReceived;
+ locals.entry.totalForwarded = locals.gate.totalForwarded;
+ locals.entry.currentBalance = locals.gate.currentBalance;
+ locals.entry.threshold = locals.gate.threshold;
+ locals.entry.createdEpoch = locals.gate.createdEpoch;
+ locals.entry.lastActivityEpoch = locals.gate.lastActivityEpoch;
+
+ for (locals.j = 0; locals.j < QUGATE_MAX_RECIPIENTS; locals.j++)
+ {
+ locals.entry.recipients.set(locals.j, locals.gate.recipients.get(locals.j));
+ locals.entry.ratios.set(locals.j, locals.gate.ratios.get(locals.j));
+ }
+
+ output.gates.set(locals.i, locals.entry);
+ }
+ }
+ }
+
+ // Fee query
+ PUBLIC_FUNCTION(getFees)
+ {
+ output.creationFee = state._creationFee;
+ output.currentCreationFee = state._creationFee * (1 + QPI::div(state._activeGates, QUGATE_FEE_ESCALATION_STEP));
+ output.minSendAmount = state._minSendAmount;
+ output.expiryEpochs = state._expiryEpochs;
+ }
+
+ // =============================================
+ // Registration
+ // =============================================
+
+ REGISTER_USER_FUNCTIONS_AND_PROCEDURES()
+ {
+ REGISTER_USER_PROCEDURE(createGate, 1);
+ REGISTER_USER_PROCEDURE(sendToGate, 2);
+ REGISTER_USER_PROCEDURE(closeGate, 3);
+ REGISTER_USER_PROCEDURE(updateGate, 4);
+ REGISTER_USER_FUNCTION(getGate, 5);
+ REGISTER_USER_FUNCTION(getGateCount, 6);
+ REGISTER_USER_FUNCTION(getGatesByOwner, 7);
+ REGISTER_USER_FUNCTION(getGateBatch, 8); //
+ REGISTER_USER_FUNCTION(getFees, 9); //
+ }
+
+ // =============================================
+ // System procedures
+ // =============================================
+
+ INITIALIZE()
+ {
+ state._gateCount = 0;
+ state._activeGates = 0;
+ state._freeCount = 0;
+ state._totalBurned = 0; //
+ state._creationFee = QUGATE_DEFAULT_CREATION_FEE; //
+ state._minSendAmount = QUGATE_DEFAULT_MIN_SEND; //
+ state._expiryEpochs = QUGATE_DEFAULT_EXPIRY_EPOCHS; //
+ }
+
+ BEGIN_EPOCH() {}
+
+ END_EPOCH_WITH_LOCALS()
+ {
+ // Expire inactive gates
+ for (locals.i = 0; locals.i < state._gateCount; locals.i++)
+ {
+ locals.gate = state._gates.get(locals.i);
+
+ if (locals.gate.active == 1 && state._expiryEpochs > 0)
+ {
+ if (qpi.epoch() - locals.gate.lastActivityEpoch >= state._expiryEpochs)
+ {
+ // Refund any held balance (THRESHOLD mode)
+ if (locals.gate.currentBalance > 0)
+ {
+ qpi.transfer(locals.gate.owner, locals.gate.currentBalance);
+ locals.gate.currentBalance = 0;
+ }
+
+ locals.gate.active = 0;
+ state._gates.set(locals.i, locals.gate);
+ state._activeGates -= 1;
+
+ // Push slot onto free-list
+ state._freeSlots.set(state._freeCount, locals.i);
+ state._freeCount += 1;
+
+ // Log expiry
+ locals.logger._contractIndex = CONTRACT_INDEX;
+ locals.logger._type = QUGATE_LOG_GATE_EXPIRED;
+ locals.logger.gateId = locals.i + 1;
+ locals.logger.sender = locals.gate.owner;
+ locals.logger.amount = 0;
+ LOG_INFO(locals.logger);
+ }
+ }
+ }
+
+ // TODO: Check shareholder proposals and apply fee/expiry changes
+ // When DEFINE_SHAREHOLDER_PROPOSAL_STORAGE is enabled:
+ // - Check if any proposal passed quorum
+ // - If so, update state._creationFee, state._minSendAmount, state._expiryEpochs
+ // - Log fee change event
+ }
+
+ BEGIN_TICK() {}
+ END_TICK() {}
+
+};
+
+
diff --git a/test/contract_qugate.cpp b/test/contract_qugate.cpp
new file mode 100644
index 000000000..4bd7538a1
--- /dev/null
+++ b/test/contract_qugate.cpp
@@ -0,0 +1,665 @@
+#define NO_UEFI
+
+#include "contract_testing.h"
+
+constexpr uint16 PROC_CREATE_GATE = 1;
+constexpr uint16 PROC_SEND_TO_GATE = 2;
+constexpr uint16 PROC_CLOSE_GATE = 3;
+constexpr uint16 PROC_UPDATE_GATE = 4;
+constexpr uint16 FUNC_GET_GATE = 5;
+constexpr uint16 FUNC_GET_GATE_COUNT = 6;
+constexpr uint16 FUNC_GET_GATES_BY_OWNER = 7;
+constexpr uint16 FUNC_GET_GATE_BATCH = 8;
+constexpr uint16 FUNC_GET_FEES = 9;
+
+static const id userA = ID(_A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A, _A);
+static const id userB = ID(_B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B, _B);
+static const id userC = ID(_C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C, _C);
+
+class ContractTestingQuGate : protected ContractTesting
+{
+public:
+ ContractTestingQuGate()
+ {
+ initEmptySpectrum();
+ initEmptyUniverse();
+ INIT_CONTRACT(QUGATE);
+ system.epoch = contractDescriptions[QUGATE_CONTRACT_INDEX].constructionEpoch;
+ callSystemProcedure(QUGATE_CONTRACT_INDEX, INITIALIZE);
+
+ // Fund test users
+ increaseEnergy(userA, 1000000000LL);
+ increaseEnergy(userB, 1000000000LL);
+ increaseEnergy(userC, 1000000000LL);
+ }
+
+ QUGATE::createGate_output createGate(const id& user, uint8 mode, uint8 recipientCount,
+ const id* recipients, const uint64* ratios, uint64 threshold = 0,
+ const id* allowedSenders = nullptr, uint8 allowedSenderCount = 0,
+ sint64 fee = QUGATE_DEFAULT_CREATION_FEE)
+ {
+ QUGATE::createGate_input input;
+ setMemory(input, 0);
+ input.mode = mode;
+ input.recipientCount = recipientCount;
+ for (uint8 i = 0; i < recipientCount; i++)
+ {
+ input.recipients.set(i, recipients[i]);
+ input.ratios.set(i, ratios[i]);
+ }
+ input.threshold = threshold;
+ if (allowedSenders)
+ {
+ for (uint8 i = 0; i < allowedSenderCount; i++)
+ {
+ input.allowedSenders.set(i, allowedSenders[i]);
+ }
+ }
+ input.allowedSenderCount = allowedSenderCount;
+
+ QUGATE::createGate_output output;
+ invokeUserProcedure(QUGATE_CONTRACT_INDEX, PROC_CREATE_GATE, input, output, user, fee);
+ return output;
+ }
+
+ QUGATE::sendToGate_output sendToGate(const id& user, uint64 gateId, sint64 amount)
+ {
+ QUGATE::sendToGate_input input;
+ setMemory(input, 0);
+ input.gateId = gateId;
+
+ QUGATE::sendToGate_output output;
+ invokeUserProcedure(QUGATE_CONTRACT_INDEX, PROC_SEND_TO_GATE, input, output, user, amount);
+ return output;
+ }
+
+ QUGATE::closeGate_output closeGate(const id& user, uint64 gateId)
+ {
+ QUGATE::closeGate_input input;
+ setMemory(input, 0);
+ input.gateId = gateId;
+
+ QUGATE::closeGate_output output;
+ invokeUserProcedure(QUGATE_CONTRACT_INDEX, PROC_CLOSE_GATE, input, output, user, 0);
+ return output;
+ }
+
+ QUGATE::updateGate_output updateGate(const id& user, uint64 gateId, uint8 recipientCount,
+ const id* recipients, const uint64* ratios, uint64 threshold = 0,
+ const id* allowedSenders = nullptr, uint8 allowedSenderCount = 0)
+ {
+ QUGATE::updateGate_input input;
+ setMemory(input, 0);
+ input.gateId = gateId;
+ input.recipientCount = recipientCount;
+ for (uint8 i = 0; i < recipientCount; i++)
+ {
+ input.recipients.set(i, recipients[i]);
+ input.ratios.set(i, ratios[i]);
+ }
+ input.threshold = threshold;
+ if (allowedSenders)
+ {
+ for (uint8 i = 0; i < allowedSenderCount; i++)
+ {
+ input.allowedSenders.set(i, allowedSenders[i]);
+ }
+ }
+ input.allowedSenderCount = allowedSenderCount;
+
+ QUGATE::updateGate_output output;
+ invokeUserProcedure(QUGATE_CONTRACT_INDEX, PROC_UPDATE_GATE, input, output, user, 0);
+ return output;
+ }
+
+ QUGATE::getGate_output getGate(uint64 gateId)
+ {
+ QUGATE::getGate_input input;
+ setMemory(input, 0);
+ input.gateId = gateId;
+
+ QUGATE::getGate_output output;
+ callFunction(QUGATE_CONTRACT_INDEX, FUNC_GET_GATE, input, output);
+ return output;
+ }
+
+ QUGATE::getGateCount_output getGateCount()
+ {
+ QUGATE::getGateCount_input input;
+ setMemory(input, 0);
+
+ QUGATE::getGateCount_output output;
+ callFunction(QUGATE_CONTRACT_INDEX, FUNC_GET_GATE_COUNT, input, output);
+ return output;
+ }
+
+ QUGATE::getFees_output getFees()
+ {
+ QUGATE::getFees_input input;
+ setMemory(input, 0);
+
+ QUGATE::getFees_output output;
+ callFunction(QUGATE_CONTRACT_INDEX, FUNC_GET_FEES, input, output);
+ return output;
+ }
+};
+
+// ============================================================
+// Initialization
+// ============================================================
+
+TEST(ContractQuGate, InitializeDefaults)
+{
+ ContractTestingQuGate qg;
+
+ auto fees = qg.getFees();
+ EXPECT_EQ(fees.creationFee, QUGATE_DEFAULT_CREATION_FEE);
+ EXPECT_EQ(fees.currentCreationFee, QUGATE_DEFAULT_CREATION_FEE);
+ EXPECT_EQ(fees.minSendAmount, QUGATE_DEFAULT_MIN_SEND);
+ EXPECT_EQ(fees.expiryEpochs, QUGATE_DEFAULT_EXPIRY_EPOCHS);
+
+ auto counts = qg.getGateCount();
+ EXPECT_EQ(counts.totalGates, 0ULL);
+ EXPECT_EQ(counts.activeGates, 0ULL);
+ EXPECT_EQ(counts.totalBurned, 0ULL);
+}
+
+// ============================================================
+// SPLIT mode
+// ============================================================
+
+TEST(ContractQuGate, CreateSplitGate)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB, userC};
+ uint64 ratios[] = {60, 40};
+ auto out = qg.createGate(userA, QUGATE_MODE_SPLIT, 2, recipients, ratios);
+ EXPECT_EQ(out.status, QUGATE_SUCCESS);
+ EXPECT_EQ(out.gateId, 1ULL);
+ EXPECT_EQ(out.feePaid, (uint64)QUGATE_DEFAULT_CREATION_FEE);
+
+ auto gate = qg.getGate(1);
+ EXPECT_EQ(gate.mode, QUGATE_MODE_SPLIT);
+ EXPECT_EQ(gate.recipientCount, 2);
+ EXPECT_EQ(gate.active, 1);
+
+ auto counts = qg.getGateCount();
+ EXPECT_EQ(counts.totalGates, 1ULL);
+ EXPECT_EQ(counts.activeGates, 1ULL);
+ EXPECT_EQ(counts.totalBurned, (uint64)QUGATE_DEFAULT_CREATION_FEE);
+}
+
+TEST(ContractQuGate, SplitEvenDistribution)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB, userC};
+ uint64 ratios[] = {50, 50};
+ qg.createGate(userA, QUGATE_MODE_SPLIT, 2, recipients, ratios);
+
+ long long balB_before = getBalance(userB);
+ long long balC_before = getBalance(userC);
+
+ qg.sendToGate(userA, 1, 10000);
+
+ long long balB_after = getBalance(userB);
+ long long balC_after = getBalance(userC);
+
+ EXPECT_EQ(balB_after - balB_before, 5000);
+ EXPECT_EQ(balC_after - balC_before, 5000);
+}
+
+TEST(ContractQuGate, SplitUnevenDistribution)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB, userC};
+ uint64 ratios[] = {70, 30};
+ qg.createGate(userA, QUGATE_MODE_SPLIT, 2, recipients, ratios);
+
+ long long balB_before = getBalance(userB);
+ long long balC_before = getBalance(userC);
+
+ qg.sendToGate(userA, 1, 10000);
+
+ EXPECT_EQ(getBalance(userB) - balB_before, 7000);
+ EXPECT_EQ(getBalance(userC) - balC_before, 3000);
+}
+
+TEST(ContractQuGate, SplitRoundingLastRecipient)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB, userC};
+ uint64 ratios[] = {33, 67};
+ qg.createGate(userA, QUGATE_MODE_SPLIT, 2, recipients, ratios);
+
+ long long balB_before = getBalance(userB);
+ long long balC_before = getBalance(userC);
+
+ qg.sendToGate(userA, 1, 10000);
+
+ long long gainB = getBalance(userB) - balB_before;
+ long long gainC = getBalance(userC) - balC_before;
+
+ // Total must equal sent amount (no dust lost)
+ EXPECT_EQ(gainB + gainC, 10000);
+}
+
+// ============================================================
+// ROUND_ROBIN mode
+// ============================================================
+
+TEST(ContractQuGate, RoundRobinCycling)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB, userC};
+ uint64 ratios[] = {1, 1};
+ qg.createGate(userA, QUGATE_MODE_ROUND_ROBIN, 2, recipients, ratios);
+
+ long long balB_start = getBalance(userB);
+ long long balC_start = getBalance(userC);
+
+ // Payment 1 -> recipient 0 (userB)
+ qg.sendToGate(userA, 1, 5000);
+ EXPECT_EQ(getBalance(userB) - balB_start, 5000);
+ EXPECT_EQ(getBalance(userC) - balC_start, 0);
+
+ // Payment 2 -> recipient 1 (userC)
+ qg.sendToGate(userA, 1, 5000);
+ EXPECT_EQ(getBalance(userB) - balB_start, 5000);
+ EXPECT_EQ(getBalance(userC) - balC_start, 5000);
+
+ // Payment 3 -> back to recipient 0 (userB)
+ qg.sendToGate(userA, 1, 5000);
+ EXPECT_EQ(getBalance(userB) - balB_start, 10000);
+ EXPECT_EQ(getBalance(userC) - balC_start, 5000);
+}
+
+// ============================================================
+// THRESHOLD mode
+// ============================================================
+
+TEST(ContractQuGate, ThresholdAccumulatesAndReleases)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB};
+ uint64 ratios[] = {100};
+ qg.createGate(userA, QUGATE_MODE_THRESHOLD, 1, recipients, ratios, 20000);
+
+ long long balB_before = getBalance(userB);
+
+ // Below threshold — should accumulate
+ qg.sendToGate(userA, 1, 10000);
+ EXPECT_EQ(getBalance(userB) - balB_before, 0);
+
+ auto gate = qg.getGate(1);
+ EXPECT_EQ(gate.currentBalance, 10000ULL);
+
+ // Reaches threshold — should forward all
+ qg.sendToGate(userA, 1, 10000);
+ EXPECT_EQ(getBalance(userB) - balB_before, 20000);
+
+ gate = qg.getGate(1);
+ EXPECT_EQ(gate.currentBalance, 0ULL);
+}
+
+// ============================================================
+// RANDOM mode
+// ============================================================
+
+TEST(ContractQuGate, RandomSelectsRecipient)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB, userC};
+ uint64 ratios[] = {1, 1};
+ qg.createGate(userA, QUGATE_MODE_RANDOM, 2, recipients, ratios);
+
+ long long balB_before = getBalance(userB);
+ long long balC_before = getBalance(userC);
+
+ // Send multiple payments — at least one should go to each recipient
+ for (int i = 0; i < 10; i++)
+ {
+ qg.sendToGate(userA, 1, 1000);
+ }
+
+ long long gainB = getBalance(userB) - balB_before;
+ long long gainC = getBalance(userC) - balC_before;
+
+ // Total must equal 10000
+ EXPECT_EQ(gainB + gainC, 10000);
+ // Each should get at least something (probabilistic, but very unlikely to fail with 10 sends)
+ EXPECT_GT(gainB, 0);
+ EXPECT_GT(gainC, 0);
+}
+
+// ============================================================
+// CONDITIONAL mode
+// ============================================================
+
+TEST(ContractQuGate, ConditionalAllowedSender)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB};
+ uint64 ratios[] = {100};
+ id allowed[] = {userA};
+ qg.createGate(userA, QUGATE_MODE_CONDITIONAL, 1, recipients, ratios, 0, allowed, 1);
+
+ long long balB_before = getBalance(userB);
+
+ auto out = qg.sendToGate(userA, 1, 10000);
+ EXPECT_EQ(out.status, QUGATE_SUCCESS);
+ EXPECT_EQ(getBalance(userB) - balB_before, 10000);
+}
+
+TEST(ContractQuGate, ConditionalRejectedSender)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB};
+ uint64 ratios[] = {100};
+ id allowed[] = {userA};
+ qg.createGate(userA, QUGATE_MODE_CONDITIONAL, 1, recipients, ratios, 0, allowed, 1);
+
+ long long balC_before = getBalance(userC);
+ long long balB_before = getBalance(userB);
+
+ auto out = qg.sendToGate(userC, 1, 10000);
+ EXPECT_EQ(out.status, (sint64)QUGATE_CONDITIONAL_REJECTED);
+
+ // userC should get funds back, userB should receive nothing
+ EXPECT_EQ(getBalance(userC), balC_before);
+ EXPECT_EQ(getBalance(userB), balB_before);
+}
+
+// ============================================================
+// Gate lifecycle
+// ============================================================
+
+TEST(ContractQuGate, CloseGateOwnerOnly)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB};
+ uint64 ratios[] = {100};
+ qg.createGate(userA, QUGATE_MODE_SPLIT, 1, recipients, ratios);
+
+ // Non-owner cannot close
+ auto out = qg.closeGate(userC, 1);
+ EXPECT_EQ(out.status, (sint64)QUGATE_UNAUTHORIZED);
+
+ auto gate = qg.getGate(1);
+ EXPECT_EQ(gate.active, 1);
+
+ // Owner can close
+ out = qg.closeGate(userA, 1);
+ EXPECT_EQ(out.status, QUGATE_SUCCESS);
+
+ gate = qg.getGate(1);
+ EXPECT_EQ(gate.active, 0);
+}
+
+TEST(ContractQuGate, UpdateGateOwnerOnly)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB, userC};
+ uint64 ratios[] = {60, 40};
+ qg.createGate(userA, QUGATE_MODE_SPLIT, 2, recipients, ratios);
+
+ // Non-owner update rejected
+ uint64 newRatios[] = {99, 1};
+ auto out = qg.updateGate(userC, 1, 2, recipients, newRatios);
+ EXPECT_EQ(out.status, (sint64)QUGATE_UNAUTHORIZED);
+
+ // Owner update succeeds
+ uint64 ratios2[] = {25, 75};
+ out = qg.updateGate(userA, 1, 2, recipients, ratios2);
+ EXPECT_EQ(out.status, QUGATE_SUCCESS);
+}
+
+TEST(ContractQuGate, SendToClosedGate)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB};
+ uint64 ratios[] = {100};
+ qg.createGate(userA, QUGATE_MODE_SPLIT, 1, recipients, ratios);
+ qg.closeGate(userA, 1);
+
+ long long balA_before = getBalance(userA);
+ auto out = qg.sendToGate(userA, 1, 5000);
+ EXPECT_EQ(out.status, (sint64)QUGATE_GATE_NOT_ACTIVE);
+}
+
+TEST(ContractQuGate, SendToInvalidGate)
+{
+ ContractTestingQuGate qg;
+
+ auto out = qg.sendToGate(userA, 9999, 5000);
+ EXPECT_EQ(out.status, (sint64)QUGATE_INVALID_GATE_ID);
+}
+
+// ============================================================
+// Error cases
+// ============================================================
+
+TEST(ContractQuGate, InvalidMode)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB};
+ uint64 ratios[] = {100};
+ auto out = qg.createGate(userA, 5, 1, recipients, ratios);
+ EXPECT_EQ(out.status, (sint64)QUGATE_INVALID_MODE);
+}
+
+TEST(ContractQuGate, InvalidRecipientCount)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB};
+ uint64 ratios[] = {100};
+ auto out = qg.createGate(userA, QUGATE_MODE_SPLIT, 0, recipients, ratios);
+ EXPECT_EQ(out.status, (sint64)QUGATE_INVALID_RECIPIENT_COUNT);
+}
+
+TEST(ContractQuGate, InvalidRatio)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB};
+ uint64 ratios[] = {QUGATE_MAX_RATIO + 1};
+ auto out = qg.createGate(userA, QUGATE_MODE_SPLIT, 1, recipients, ratios);
+ EXPECT_EQ(out.status, (sint64)QUGATE_INVALID_RATIO);
+}
+
+TEST(ContractQuGate, InsufficientFee)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB};
+ uint64 ratios[] = {100};
+ auto out = qg.createGate(userA, QUGATE_MODE_SPLIT, 1, recipients, ratios, 0, nullptr, 0, 1000);
+ EXPECT_EQ(out.status, (sint64)QUGATE_INSUFFICIENT_FEE);
+}
+
+TEST(ContractQuGate, InvalidThreshold)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB};
+ uint64 ratios[] = {100};
+ auto out = qg.createGate(userA, QUGATE_MODE_THRESHOLD, 1, recipients, ratios, 0);
+ EXPECT_EQ(out.status, (sint64)QUGATE_INVALID_THRESHOLD);
+}
+
+TEST(ContractQuGate, InvalidSenderCount)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB};
+ uint64 ratios[] = {100};
+ id allowed[] = {userA};
+ // allowedSenderCount > 8
+ auto out = qg.createGate(userA, QUGATE_MODE_CONDITIONAL, 1, recipients, ratios, 0, allowed, 9);
+ EXPECT_EQ(out.status, (sint64)QUGATE_INVALID_SENDER_COUNT);
+}
+
+// ============================================================
+// Anti-spam
+// ============================================================
+
+TEST(ContractQuGate, DustBurn)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB};
+ uint64 ratios[] = {100};
+ qg.createGate(userA, QUGATE_MODE_SPLIT, 1, recipients, ratios);
+
+ long long balB_before = getBalance(userB);
+
+ // Send below min send — should be burned
+ auto out = qg.sendToGate(userA, 1, 500);
+ EXPECT_EQ(out.status, (sint64)QUGATE_DUST_AMOUNT);
+
+ // Recipient should receive nothing
+ EXPECT_EQ(getBalance(userB), balB_before);
+}
+
+TEST(ContractQuGate, FeeOverpaymentRefund)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB};
+ uint64 ratios[] = {100};
+ long long balA_before = getBalance(userA);
+
+ // Overpay by 50000
+ auto out = qg.createGate(userA, QUGATE_MODE_SPLIT, 1, recipients, ratios, 0, nullptr, 0,
+ QUGATE_DEFAULT_CREATION_FEE + 50000);
+ EXPECT_EQ(out.status, QUGATE_SUCCESS);
+ EXPECT_EQ(out.feePaid, (uint64)QUGATE_DEFAULT_CREATION_FEE);
+
+ // Should only lose the creation fee, not the overpayment
+ long long balA_after = getBalance(userA);
+ EXPECT_EQ(balA_before - balA_after, QUGATE_DEFAULT_CREATION_FEE);
+}
+
+TEST(ContractQuGate, EscalatingFees)
+{
+ ContractTestingQuGate qg;
+
+ auto fees = qg.getFees();
+ EXPECT_EQ(fees.currentCreationFee, QUGATE_DEFAULT_CREATION_FEE);
+
+ // Fee should increase as active gates grow (tested via getFees query)
+ // At 0 active gates: multiplier = 1
+ // At QUGATE_FEE_ESCALATION_STEP active gates: multiplier = 2
+}
+
+// ============================================================
+// Free-list slot reuse
+// ============================================================
+
+TEST(ContractQuGate, SlotReuse)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB};
+ uint64 ratios[] = {100};
+
+ // Create and close a gate
+ qg.createGate(userA, QUGATE_MODE_SPLIT, 1, recipients, ratios);
+ EXPECT_EQ(qg.getGateCount().totalGates, 1ULL);
+ EXPECT_EQ(qg.getGateCount().activeGates, 1ULL);
+
+ qg.closeGate(userA, 1);
+ EXPECT_EQ(qg.getGateCount().activeGates, 0ULL);
+
+ // Create a new gate — should reuse the freed slot
+ auto out = qg.createGate(userA, QUGATE_MODE_SPLIT, 1, recipients, ratios);
+ EXPECT_EQ(out.status, QUGATE_SUCCESS);
+
+ // totalGates should still be 1 (slot reused, not allocated new)
+ EXPECT_EQ(qg.getGateCount().totalGates, 1ULL);
+ EXPECT_EQ(qg.getGateCount().activeGates, 1ULL);
+}
+
+// ============================================================
+// Query functions
+// ============================================================
+
+TEST(ContractQuGate, GetGateInvalid)
+{
+ ContractTestingQuGate qg;
+
+ auto gate = qg.getGate(9999);
+ EXPECT_EQ(gate.active, 0);
+}
+
+TEST(ContractQuGate, GetFees)
+{
+ ContractTestingQuGate qg;
+
+ auto fees = qg.getFees();
+ EXPECT_EQ(fees.creationFee, QUGATE_DEFAULT_CREATION_FEE);
+ EXPECT_EQ(fees.minSendAmount, QUGATE_DEFAULT_MIN_SEND);
+ EXPECT_EQ(fees.expiryEpochs, QUGATE_DEFAULT_EXPIRY_EPOCHS);
+}
+
+// ============================================================
+// Threshold close refund
+// ============================================================
+
+TEST(ContractQuGate, ThresholdCloseRefundsBalance)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB};
+ uint64 ratios[] = {100};
+ qg.createGate(userA, QUGATE_MODE_THRESHOLD, 1, recipients, ratios, 50000);
+
+ // Send below threshold
+ qg.sendToGate(userA, 1, 10000);
+
+ auto gate = qg.getGate(1);
+ EXPECT_EQ(gate.currentBalance, 10000ULL);
+
+ long long balA_before = getBalance(userA);
+
+ // Close gate — held balance should refund to owner
+ qg.closeGate(userA, 1);
+
+ long long balA_after = getBalance(userA);
+ EXPECT_EQ(balA_after - balA_before, 10000);
+}
+
+// ============================================================
+// Total burned tracking
+// ============================================================
+
+TEST(ContractQuGate, TotalBurnedTracking)
+{
+ ContractTestingQuGate qg;
+
+ id recipients[] = {userB};
+ uint64 ratios[] = {100};
+
+ // Create gate — fee burned
+ qg.createGate(userA, QUGATE_MODE_SPLIT, 1, recipients, ratios);
+ auto counts = qg.getGateCount();
+ EXPECT_EQ(counts.totalBurned, (uint64)QUGATE_DEFAULT_CREATION_FEE);
+
+ // Send dust — also burned
+ qg.sendToGate(userA, 1, 500);
+ counts = qg.getGateCount();
+ EXPECT_EQ(counts.totalBurned, (uint64)QUGATE_DEFAULT_CREATION_FEE + 500);
+}
diff --git a/test/test.vcxproj b/test/test.vcxproj
index 4b74f2e39..e0c10fd76 100644
--- a/test/test.vcxproj
+++ b/test/test.vcxproj
@@ -147,6 +147,7 @@
+
diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters
index 6e49bac25..d2d8b1473 100644
--- a/test/test.vcxproj.filters
+++ b/test/test.vcxproj.filters
@@ -29,6 +29,7 @@
+