diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj
index 599025e8..40e230bd 100644
--- a/src/Qubic.vcxproj
+++ b/src/Qubic.vcxproj
@@ -49,6 +49,7 @@
+
diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters
index 0aec3c8c..78bedbd7 100644
--- a/src/Qubic.vcxproj.filters
+++ b/src/Qubic.vcxproj.filters
@@ -308,6 +308,12 @@
contracts
+
+ contracts
+
+
+ contracts
+
contracts
diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h
index 7b2ff71c..d9ffff07 100644
--- a/src/contract_core/contract_def.h
+++ b/src/contract_core/contract_def.h
@@ -278,6 +278,16 @@
#endif
+#undef CONTRACT_INDEX
+#undef CONTRACT_STATE_TYPE
+#undef CONTRACT_STATE2_TYPE
+
+#define QSB_CONTRACT_INDEX 27
+#define CONTRACT_INDEX QSB_CONTRACT_INDEX
+#define CONTRACT_STATE_TYPE QSB
+#define CONTRACT_STATE2_TYPE QSB2
+#include "contracts/QubicSolanaBridge.h"
+
// new contracts should be added above this line
#ifdef INCLUDE_CONTRACT_TEST_EXAMPLES
diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h
new file mode 100644
index 00000000..24f59488
--- /dev/null
+++ b/src/contracts/QubicSolanaBridge.h
@@ -0,0 +1,1958 @@
+using namespace QPI;
+
+// ---------------------------------------------------------------------
+// Constants / configuration
+// ---------------------------------------------------------------------
+
+static constexpr uint32 QSB_MAX_ORACLES = 64;
+static constexpr uint32 QSB_MAX_PAUSERS = 32;
+static constexpr uint32 QSB_MAX_FILLED_ORDERS = 2048;
+static constexpr uint32 QSB_MAX_LOCKED_ORDERS = 1024;
+static constexpr uint32 QSB_MAX_BPS_FEE = 1000; // max 10% fee (1000 / 10000)
+static constexpr uint32 QSB_MAX_PROTOCOL_FEE = 100; // max 100% of bps fee
+
+// Domain-prefixed order message for K12 hashing and signature verification.
+// Layout: 245 bytes total. protocolName is padded to 16 (next power of 2 above 11).
+struct QSBOrderMessage
+{
+ uint32 protocolNameLen; // 0: always 11
+ Array protocolName; // 4: QubicBridge (11 used, 5 zero-padded)
+ uint32 protocolVersionLen; // 20: always 1
+ Array protocolVersion; // 24: version byte (49 = ASCII '1')
+ Array contractAddress; // 25: destination contract address (LE-padded index)
+ uint32 networkIn; // 57
+ uint32 networkOut; // 61
+ Array tokenIn; // 65
+ Array tokenOut; // 97
+ Array fromAddress; // 129
+ Array toAddress; // 161
+ uint64 amount; // 193
+ uint64 relayerFee; // 201
+ Array nonce; // 209
+ uint32 orderEra; // 241
+};
+static constexpr uint32 QSB_QUERY_MAX_PAGE_SIZE = 64; // max entries per paginated query
+
+// Log types for QSB contract (no enums allowed in contracts)
+static constexpr uint32 QSBLogLock = 1;
+static constexpr uint32 QSBLogOverrideLock = 2;
+static constexpr uint32 QSBLogUnlock = 3;
+static constexpr uint32 QSBLogPaused = 4;
+static constexpr uint32 QSBLogUnpaused = 5;
+static constexpr uint32 QSBLogAdminTransferred = 6;
+static constexpr uint32 QSBLogThresholdUpdated = 7;
+static constexpr uint32 QSBLogRoleGranted = 8;
+static constexpr uint32 QSBLogRoleRevoked = 9;
+static constexpr uint32 QSBLogFeeParametersUpdated = 10;
+
+// Generic reason codes for logging
+static constexpr uint8 QSBReasonNone = 0;
+static constexpr uint8 QSBReasonPaused = 1;
+static constexpr uint8 QSBReasonInvalidAmount = 2;
+static constexpr uint8 QSBReasonInsufficientReward = 3;
+static constexpr uint8 QSBReasonNonceUsed = 4;
+static constexpr uint8 QSBReasonNoSpace = 5;
+static constexpr uint8 QSBReasonNotSender = 6;
+static constexpr uint8 QSBReasonBadRelayerFee = 7;
+static constexpr uint8 QSBReasonNoOracles = 8;
+static constexpr uint8 QSBReasonThresholdFailed = 9;
+static constexpr uint8 QSBReasonAlreadyFilled = 10;
+static constexpr uint8 QSBReasonInvalidSignature = 11;
+static constexpr uint8 QSBReasonDuplicateSigner = 12;
+static constexpr uint8 QSBReasonNotAdmin = 13;
+static constexpr uint8 QSBReasonNotAdminOrPauser = 14;
+static constexpr uint8 QSBReasonInvalidThreshold = 15;
+static constexpr uint8 QSBReasonRoleExists = 16;
+static constexpr uint8 QSBReasonRoleMissing = 17;
+static constexpr uint8 QSBReasonInvalidFeeParams = 18;
+static constexpr uint8 QSBReasonTransferFailed = 19;
+static constexpr uint8 QSBReasonEraMismatch = 20;
+// 21 reserved for future use
+
+struct QSB2
+{
+};
+
+struct QSB : public ContractBase
+{
+public:
+ // Role identifiers for addRole / removeRole
+ enum class Role : uint8
+ {
+ Oracle = 1,
+ Pauser = 2
+ };
+
+ // ---------------------------------------------------------------------
+ // Core data structures
+ // ---------------------------------------------------------------------
+
+ struct Order
+ {
+ id fromAddress;
+ id toAddress;
+ Array tokenIn;
+ Array tokenOut;
+ uint64 amount;
+ uint64 relayerFee;
+ uint32 networkIn;
+ uint32 networkOut;
+ Array nonce;
+ uint32 orderEra;
+ };
+
+ // Compact order-hash representation (K12 digest)
+ typedef Array OrderHash;
+
+ // Signature wrapper compatible with QPI::signatureValidity
+ struct SignatureData
+ {
+ id signer; // oracle id (public key)
+ Array signature; // raw 64-byte signature
+ };
+
+ // Storage entry for filledOrders mapping
+ struct FilledOrderEntry
+ {
+ OrderHash hash;
+ bit used;
+ };
+
+ // Storage entry for role mappings (oracles / pausers)
+ struct RoleEntry
+ {
+ id account;
+ bit active;
+ };
+
+ // Storage entry for lock() orders (for overrideLock / off-chain reference)
+ struct LockedOrderEntry
+ {
+ id sender;
+ uint64 amount;
+ uint64 relayerFee;
+ uint32 networkOut;
+ uint32 nonce;
+ Array toAddress;
+ OrderHash orderHash;
+ uint32 lockEpoch;
+ uint32 orderEra;
+ bit active;
+ };
+
+ // Logging messages
+ struct QSBLogLockMessage
+ {
+ uint32 _contractIndex;
+ uint32 _type;
+ id from;
+ Array to;
+ uint64 amount;
+ uint64 relayerFee;
+ uint32 networkOut;
+ uint32 nonce;
+ OrderHash orderHash;
+ uint8 success;
+ uint8 reasonCode;
+ uint32 orderEra;
+ sint8 _terminator;
+ };
+
+ struct QSBLogOverrideLockMessage
+ {
+ uint32 _contractIndex;
+ uint32 _type;
+ id from;
+ Array to;
+ uint64 amount;
+ uint64 relayerFee;
+ uint32 networkOut;
+ uint32 nonce;
+ OrderHash orderHash;
+ uint8 success;
+ uint8 reasonCode;
+ uint32 orderEra;
+ sint8 _terminator;
+ };
+
+ struct QSBLogUnlockMessage
+ {
+ uint32 _contractIndex;
+ uint32 _type;
+ OrderHash orderHash;
+ id toAddress;
+ uint64 amount;
+ uint64 relayerFee;
+ id relayer;
+ uint8 success;
+ uint8 reasonCode;
+ uint32 orderEra;
+ sint8 _terminator;
+ };
+
+ struct QSBLogAdminTransferredMessage
+ {
+ uint32 _contractIndex;
+ uint32 _type;
+ id previousAdmin;
+ id newAdmin;
+ uint8 success;
+ uint8 reasonCode;
+ sint8 _terminator;
+ };
+
+ struct QSBLogThresholdUpdatedMessage
+ {
+ uint32 _contractIndex;
+ uint32 _type;
+ uint8 oldThreshold;
+ uint8 newThreshold;
+ uint8 success;
+ uint8 reasonCode;
+ sint8 _terminator;
+ };
+
+ struct QSBLogRoleMessage
+ {
+ uint32 _contractIndex;
+ uint32 _type;
+ uint8 role;
+ id account;
+ id caller;
+ uint8 success;
+ uint8 reasonCode;
+ sint8 _terminator;
+ };
+
+ struct QSBLogPausedMessage
+ {
+ uint32 _contractIndex;
+ uint32 _type;
+ id caller;
+ uint8 success;
+ uint8 reasonCode;
+ sint8 _terminator;
+ };
+
+ struct QSBLogFeeParametersUpdatedMessage
+ {
+ uint32 _contractIndex;
+ uint32 _type;
+ uint32 bpsFee;
+ uint32 protocolFee;
+ id protocolFeeRecipient;
+ id oracleFeeRecipient;
+ uint8 success;
+ uint8 reasonCode;
+ sint8 _terminator;
+ };
+
+ // ---------------------------------------------------------------------
+ // User-facing I/O structures
+ // ---------------------------------------------------------------------
+
+ // 1) lock()
+ struct Lock_input
+ {
+ // Recipient on Solana (fixed-size buffer, zero-padded)
+ uint64 amount;
+ uint64 relayerFee;
+ Array toAddress;
+ uint32 networkOut;
+ uint32 nonce;
+ };
+
+ struct Lock_output
+ {
+ OrderHash orderHash;
+ bit success;
+ };
+
+ // 2) overrideLock()
+ struct OverrideLock_input
+ {
+ Array toAddress;
+ uint64 relayerFee;
+ uint32 nonce;
+ };
+
+ struct OverrideLock_output
+ {
+ OrderHash orderHash;
+ bit success;
+ };
+
+ // 3) unlock()
+ struct Unlock_input
+ {
+ Order order;
+ uint32 numSignatures;
+ Array signatures;
+ };
+
+ struct Unlock_output
+ {
+ OrderHash orderHash;
+ bit success;
+ };
+
+ // 4) transferAdmin()
+ struct TransferAdmin_input
+ {
+ id newAdmin;
+ };
+
+ struct TransferAdmin_output
+ {
+ bit success;
+ };
+
+ // 5) editOracleThreshold()
+ struct EditOracleThreshold_input
+ {
+ uint8 newThreshold;
+ };
+
+ struct EditOracleThreshold_output
+ {
+ uint8 oldThreshold;
+ bit success;
+ };
+
+ // 6) addRole()
+ struct AddRole_input
+ {
+ id account;
+ uint8 role; // see Role enum
+ };
+
+ struct AddRole_output
+ {
+ bit success;
+ };
+
+ // 7) removeRole()
+ struct RemoveRole_input
+ {
+ id account;
+ uint8 role;
+ };
+
+ struct RemoveRole_output
+ {
+ bit success;
+ };
+
+ // 8) pause() / unpause()
+ struct Pause_input
+ {
+ };
+
+ struct Pause_output
+ {
+ bit success;
+ };
+
+ typedef Pause_input Unpause_input;
+ typedef Pause_output Unpause_output;
+
+ // 9) editFeeParameters()
+ struct EditFeeParameters_input
+ {
+ id protocolFeeRecipient; // updated when not zero-id
+ id oracleFeeRecipient; // updated when not zero-id
+ uint32 bpsFee; // basis points fee (0..10000)
+ uint32 protocolFee; // share of BPS fee for protocol (0..100)
+ };
+
+ struct EditFeeParameters_output
+ {
+ bit success;
+ };
+
+ // ---------------------------------------------------------------------
+ // View / frontend helper functions
+ // ---------------------------------------------------------------------
+
+ struct GetConfig_input
+ {
+ };
+
+ struct GetConfig_output
+ {
+ id admin;
+ id protocolFeeRecipient;
+ id oracleFeeRecipient;
+ uint32 bpsFee;
+ uint32 protocolFee;
+ uint32 oracleCount;
+ uint32 pauserCount;
+ uint8 oracleThreshold;
+ bit paused;
+ uint32 orderEra;
+ };
+
+ struct IsOracle_input
+ {
+ id account;
+ };
+
+ struct IsOracle_output
+ {
+ bit isOracle;
+ };
+
+ struct IsPauser_input
+ {
+ id account;
+ };
+
+ struct IsPauser_output
+ {
+ bit isPauser;
+ };
+
+ struct GetLockedOrder_input
+ {
+ uint32 nonce;
+ };
+
+ struct GetLockedOrder_output
+ {
+ bit exists;
+ LockedOrderEntry order;
+ };
+
+ struct IsOrderFilled_input
+ {
+ OrderHash hash;
+ };
+
+ struct IsOrderFilled_output
+ {
+ bit filled;
+ };
+
+ // ComputeOrderHash: canonical hash for Unlock verification
+ struct ComputeOrderHash_input
+ {
+ Order order;
+ };
+
+ struct ComputeOrderHash_output
+ {
+ OrderHash hash;
+ };
+
+ // GetOracles: bulk enumeration of all oracle accounts
+ struct GetOracles_input
+ {
+ };
+
+ struct GetOracles_output
+ {
+ uint32 count;
+ Array accounts;
+ };
+
+ // GetPausers: bulk enumeration of all pauser accounts
+ struct GetPausers_input
+ {
+ };
+
+ struct GetPausers_output
+ {
+ uint32 count;
+ Array accounts;
+ };
+
+ // GetLockedOrders: paginated enumeration of active locked orders
+ struct GetLockedOrders_input
+ {
+ uint32 offset; // skip this many active entries
+ uint32 limit; // return up to this many (capped at QSB_QUERY_MAX_PAGE_SIZE)
+ };
+
+ struct GetLockedOrders_output
+ {
+ uint32 totalActive;
+ uint32 returned;
+ Array entries;
+ };
+
+ // GetFilledOrders: paginated enumeration of filled order hashes
+ struct GetFilledOrders_input
+ {
+ uint32 offset; // skip this many filled entries
+ uint32 limit; // return up to this many (capped at QSB_QUERY_MAX_PAGE_SIZE)
+ };
+
+ struct GetFilledOrders_output
+ {
+ uint32 totalActive;
+ uint32 returned;
+ Array hashes;
+ };
+
+ // ---------------------------------------------------------------------
+ // State data (accessible via state.get() / state.mut() in procedures)
+ // ---------------------------------------------------------------------
+ struct StateData
+ {
+ id admin;
+ id protocolFeeRecipient; // receives protocolFeeAmount
+ id oracleFeeRecipient; // receives oracleFeeAmount
+ Array oracles;
+ Array pausers;
+ Array filledOrders;
+ Array lockedOrders;
+ uint32 lastLockedOrdersNextOverwriteIdx;
+ uint32 lastFilledOrdersNextOverwriteIdx;
+ uint32 oracleCount;
+ uint32 pauserCount;
+ uint32 bpsFee; // fee taken in BPS (base 10000) from netAmount
+ uint32 protocolFee; // percent of BPS fee sent to protocol (base 100)
+ uint8 oracleThreshold; // percent [1..100]
+ bit paused;
+ uint32 orderEra;
+ };
+
+protected:
+
+ // ---------------------------------------------------------------------
+ // Internal helpers
+ // ---------------------------------------------------------------------
+
+ // Truncate digest to OrderHash (full 32 bytes)
+ inline static void digestToOrderHash(const id& digest, OrderHash& outHash)
+ {
+ // Copy digest directly to OrderHash (both are 32 bytes)
+ // Use setMem which handles 32-byte types specially
+ outHash.setMem(digest);
+ }
+
+ inline static void initDomainPrefix(QSBOrderMessage& msg)
+ {
+ setMemory(msg, 0);
+ msg.protocolNameLen = 11;
+ msg.protocolName.set(0, 81); // Q
+ msg.protocolName.set(1, 117); // u
+ msg.protocolName.set(2, 98); // b
+ msg.protocolName.set(3, 105); // i
+ msg.protocolName.set(4, 99); // c
+ msg.protocolName.set(5, 66); // B
+ msg.protocolName.set(6, 114); // r
+ msg.protocolName.set(7, 105); // i
+ msg.protocolName.set(8, 100); // d
+ msg.protocolName.set(9, 103); // g
+ msg.protocolName.set(10, 101); // e
+ msg.protocolVersionLen = 1;
+ msg.protocolVersion.set(0, 49); // 1
+ msg.contractAddress.set(0, (uint8)(CONTRACT_INDEX & 0xFF));
+ msg.contractAddress.set(1, (uint8)((CONTRACT_INDEX >> 8) & 0xFF));
+ }
+
+ inline static void buildOrderMessage(
+ QSBOrderMessage& msg,
+ const Order& order,
+ OrderHash& tmpIdBytes,
+ uint32 i)
+ {
+ initDomainPrefix(msg);
+ msg.networkIn = order.networkIn;
+ msg.networkOut = order.networkOut;
+ for (i = 0; i < 32; ++i) msg.tokenIn.set(i, order.tokenIn.get(i));
+ for (i = 0; i < 32; ++i) msg.tokenOut.set(i, order.tokenOut.get(i));
+ tmpIdBytes.setMem(order.fromAddress);
+ for (i = 0; i < 32; ++i) msg.fromAddress.set(i, tmpIdBytes.get(i));
+ tmpIdBytes.setMem(order.toAddress);
+ for (i = 0; i < 32; ++i) msg.toAddress.set(i, tmpIdBytes.get(i));
+ msg.amount = order.amount;
+ msg.relayerFee = order.relayerFee;
+ for (i = 0; i < 32; ++i) msg.nonce.set(i, order.nonce.get(i));
+ msg.orderEra = order.orderEra;
+ }
+
+ // Check if caller is current admin (or if admin is not yet set, allow bootstrap)
+ inline static bool isAdmin(const QPI::ContractState& state, const id& who)
+ {
+ if (isZero(state.get().admin))
+ return true;
+ return who == state.get().admin;
+ }
+
+ // Check if caller is admin or has pauser role
+ inline static bool isAdminOrPauser(const QPI::ContractState& state, const id& who, uint32 i)
+ {
+ if (isAdmin(state, who))
+ return true;
+
+ for (i = 0; i < state.get().pausers.capacity(); ++i)
+ {
+ if (state.get().pausers.get(i).active && state.get().pausers.get(i).account == who)
+ return true;
+ }
+ return false;
+ }
+
+ // Find oracle index; returns NULL_INDEX if not found
+ inline static sint64 findOracleIndex(const QPI::ContractState& state, const id& account, uint32 i)
+ {
+ for (i = 0; i < state.get().oracles.capacity(); ++i)
+ {
+ if (state.get().oracles.get(i).active && state.get().oracles.get(i).account == account)
+ return (sint32)i;
+ }
+ return NULL_INDEX;
+ }
+
+ // Find pauser index; returns NULL_INDEX if not found
+ inline static sint64 findPauserIndex(const QPI::ContractState& state, const id& account, uint32 i)
+ {
+ for (i = 0; i < state.get().pausers.capacity(); ++i)
+ {
+ if (state.get().pausers.get(i).active && state.get().pausers.get(i).account == account)
+ return (sint32)i;
+ }
+ return NULL_INDEX;
+ }
+
+ // Clear a locked order entry so its slot can be reused
+ inline static void clearLockedOrderEntry(LockedOrderEntry& entry)
+ {
+ entry.active = false;
+ entry.lockEpoch = 0;
+ entry.orderEra = 0;
+ entry.sender = 0;
+ entry.networkOut = 0;
+ entry.amount = 0;
+ entry.relayerFee = 0;
+ entry.nonce = 0;
+ setMemory(entry.toAddress, 0);
+ setMemory(entry.orderHash, 0);
+ }
+
+ // Mark an orderHash as filled (idempotent, ring-buffer storage)
+ inline static void markOrderFilled(QPI::ContractState& state, const OrderHash& hash, uint32 i, uint32 j, bool same, FilledOrderEntry& entry)
+ {
+ // First, see if it already exists
+ for (i = 0; i < state.get().filledOrders.capacity(); ++i)
+ {
+ entry = state.get().filledOrders.get(i);
+ if (entry.used)
+ {
+ same = true;
+ for (j = 0; j < hash.capacity(); ++j)
+ {
+ if (entry.hash.get(j) != hash.get(j))
+ {
+ same = false;
+ break;
+ }
+ }
+ if (same)
+ return;
+ }
+ }
+
+ // Otherwise, insert into the next ring-buffer slot and advance the index.
+ i = state.get().lastFilledOrdersNextOverwriteIdx;
+ entry = state.get().filledOrders.get(i);
+ entry.hash = hash;
+ entry.used = true;
+ state.mut().filledOrders.set(i, entry);
+ j = (state.get().lastFilledOrdersNextOverwriteIdx + 1) & (QSB_MAX_FILLED_ORDERS - 1);
+ state.mut().lastFilledOrdersNextOverwriteIdx = j;
+ if (j == 0)
+ {
+ state.mut().orderEra = state.get().orderEra + 1;
+ }
+ }
+
+ // Check whether an orderHash has already been filled
+ inline static bit isOrderFilled(const QPI::ContractState& state, const OrderHash& hash, uint32 i, uint32 j, bool same, FilledOrderEntry& entry)
+ {
+ for (i = 0; i < state.get().filledOrders.capacity(); ++i)
+ {
+ entry = state.get().filledOrders.get(i);
+ if (!entry.used)
+ continue;
+
+ same = true;
+ for (j = 0; j < hash.capacity(); ++j)
+ {
+ if (entry.hash.get(j) != hash.get(j))
+ {
+ same = false;
+ break;
+ }
+ }
+ if (same)
+ return true;
+ }
+ return false;
+ }
+
+ // Find index of locked order by nonce; returns NULL_INDEX if not found
+ inline static sint64 findLockedOrderIndexByNonce(const QPI::ContractState& state, uint32 nonce, uint32 i)
+ {
+ for (i = 0; i < QSB_MAX_LOCKED_ORDERS; ++i)
+ {
+ if (state.get().lockedOrders.get(i).active && state.get().lockedOrders.get(i).nonce == nonce)
+ return (sint32)i;
+ }
+ return NULL_INDEX;
+ }
+
+public:
+ // ---------------------------------------------------------------------
+ // Core user procedures
+ // ---------------------------------------------------------------------
+
+ struct Lock_locals
+ {
+ id digest;
+ LockedOrderEntry existing;
+ Order tmpOrder;
+ LockedOrderEntry entry;
+ QSBOrderMessage msgBuffer;
+ OrderHash tmpIdBytes;
+ uint32 i;
+ QSBLogLockMessage logMsg;
+ };
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(Lock)
+ {
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogLock;
+ locals.logMsg.from = qpi.invocator();
+ copyFromBuffer(locals.logMsg.to, input.toAddress);
+ locals.logMsg.amount = input.amount;
+ locals.logMsg.relayerFee = input.relayerFee;
+ locals.logMsg.networkOut = input.networkOut;
+ locals.logMsg.nonce = input.nonce;
+ setMemory(locals.logMsg.orderHash, 0);
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonNone;
+ locals.logMsg._terminator = 0;
+
+ output.success = false;
+ setMemory(output.orderHash, 0);
+
+ // Must not be paused
+ if (state.get().paused)
+ {
+ // Refund attached funds if any
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+ locals.logMsg.reasonCode = QSBReasonPaused;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ // Basic validation
+ if (input.amount == 0 || input.relayerFee >= input.amount)
+ {
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+ locals.logMsg.reasonCode = QSBReasonInvalidAmount;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ // Ensure funds sent with call match the amount to be locked
+ if (qpi.invocationReward() < (sint64)input.amount)
+ {
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+ locals.logMsg.reasonCode = QSBReasonInsufficientReward;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ // Any excess over `amount` is refunded
+ if (qpi.invocationReward() > (sint64)input.amount)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward() - input.amount);
+ }
+
+ // Funds equal to `amount` now remain locked in the contract balance
+
+ // Ensure nonce unused
+ if (findLockedOrderIndexByNonce(state, input.nonce, 0) != NULL_INDEX)
+ {
+ // Nonce already used; reject
+ qpi.transfer(qpi.invocator(), input.amount);
+ locals.logMsg.reasonCode = QSBReasonNonceUsed;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ locals.tmpOrder.networkIn = 1;
+ locals.tmpOrder.networkOut = input.networkOut;
+ setMemory(locals.tmpOrder.tokenIn, 0);
+ setMemory(locals.tmpOrder.tokenOut, 0);
+ locals.tmpOrder.fromAddress = qpi.invocator();
+ locals.tmpOrder.toAddress = NULL_ID;
+ locals.tmpOrder.amount = input.amount;
+ locals.tmpOrder.relayerFee = input.relayerFee;
+ setMemory(locals.tmpOrder.nonce, 0);
+ locals.tmpOrder.nonce.set(0, (uint8)(input.nonce & 0xFF));
+ locals.tmpOrder.nonce.set(1, (uint8)((input.nonce >> 8) & 0xFF));
+ locals.tmpOrder.nonce.set(2, (uint8)((input.nonce >> 16) & 0xFF));
+ locals.tmpOrder.nonce.set(3, (uint8)((input.nonce >> 24) & 0xFF));
+ locals.tmpOrder.orderEra = state.get().orderEra;
+
+ buildOrderMessage(locals.msgBuffer, locals.tmpOrder, locals.tmpIdBytes, locals.i);
+ locals.digest = qpi.K12(locals.msgBuffer);
+ digestToOrderHash(locals.digest, output.orderHash);
+ locals.logMsg.orderHash = output.orderHash;
+ locals.logMsg.orderEra = state.get().orderEra;
+
+ // Persist locked order so that overrideLock or off-chain tooling can reference it.
+ locals.entry.active = true;
+ locals.entry.sender = qpi.invocator();
+ locals.entry.networkOut = input.networkOut;
+ locals.entry.amount = input.amount;
+ locals.entry.relayerFee = input.relayerFee;
+ locals.entry.nonce = input.nonce;
+ copyMemory(locals.entry.toAddress, input.toAddress);
+ locals.entry.orderHash = output.orderHash;
+ locals.entry.lockEpoch = qpi.epoch();
+ locals.entry.orderEra = state.get().orderEra;
+ state.mut().lockedOrders.set(state.get().lastLockedOrdersNextOverwriteIdx, locals.entry);
+
+ // always overwrite the next slot, wrapping around with a power-of-two mask.
+ state.mut().lastLockedOrdersNextOverwriteIdx = (state.get().lastLockedOrdersNextOverwriteIdx + 1) & (QSB_MAX_LOCKED_ORDERS - 1);
+
+ output.success = true;
+ locals.logMsg.success = 1;
+ locals.logMsg.reasonCode = QSBReasonNone;
+ LOG_INFO(locals.logMsg);
+ }
+
+ struct OverrideLock_locals
+ {
+ LockedOrderEntry entry;
+ Order tmpOrder;
+ id digest;
+ QSBOrderMessage msgBuffer;
+ OrderHash tmpIdBytes;
+ sint64 idx;
+ uint32 i;
+ QSBLogOverrideLockMessage logMsg;
+ };
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(OverrideLock)
+ {
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogOverrideLock;
+ locals.logMsg.from = qpi.invocator();
+ setMemory(locals.logMsg.to, 0);
+ locals.logMsg.amount = 0;
+ locals.logMsg.relayerFee = 0;
+ locals.logMsg.networkOut = 0;
+ locals.logMsg.nonce = input.nonce;
+ setMemory(locals.logMsg.orderHash, 0);
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonNone;
+ locals.logMsg._terminator = 0;
+ output.success = false;
+ setMemory(output.orderHash, 0);
+
+ // Always refund invocationReward (locking was done in original lock() call)
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+
+ // Contract must not be paused
+ if (state.get().paused)
+ {
+ locals.logMsg.reasonCode = QSBReasonPaused;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ // Find existing order by nonce
+ locals.idx = findLockedOrderIndexByNonce(state, input.nonce, 0);
+ if (locals.idx == NULL_INDEX)
+ {
+ locals.logMsg.reasonCode = QSBReasonNonceUsed;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ locals.entry = state.get().lockedOrders.get((uint32)locals.idx);
+
+ // Only original sender can override
+ if (locals.entry.sender != qpi.invocator())
+ {
+ locals.logMsg.reasonCode = QSBReasonNotSender;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ // Validate new relayer fee
+ if (input.relayerFee >= locals.entry.amount)
+ {
+ locals.logMsg.reasonCode = QSBReasonBadRelayerFee;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ // Update mutable fields
+ copyMemory(locals.entry.toAddress, input.toAddress);
+ locals.entry.relayerFee = input.relayerFee;
+
+ locals.tmpOrder.networkIn = 1;
+ locals.tmpOrder.networkOut = locals.entry.networkOut;
+ setMemory(locals.tmpOrder.tokenIn, 0);
+ setMemory(locals.tmpOrder.tokenOut, 0);
+ locals.tmpOrder.fromAddress = locals.entry.sender;
+ locals.tmpOrder.toAddress = NULL_ID;
+ locals.tmpOrder.amount = locals.entry.amount;
+ locals.tmpOrder.relayerFee = locals.entry.relayerFee;
+ setMemory(locals.tmpOrder.nonce, 0);
+ locals.tmpOrder.nonce.set(0, (uint8)(locals.entry.nonce & 0xFF));
+ locals.tmpOrder.nonce.set(1, (uint8)((locals.entry.nonce >> 8) & 0xFF));
+ locals.tmpOrder.nonce.set(2, (uint8)((locals.entry.nonce >> 16) & 0xFF));
+ locals.tmpOrder.nonce.set(3, (uint8)((locals.entry.nonce >> 24) & 0xFF));
+ locals.tmpOrder.orderEra = locals.entry.orderEra; // preserve original era
+
+ buildOrderMessage(locals.msgBuffer, locals.tmpOrder, locals.tmpIdBytes, locals.i);
+ locals.digest = qpi.K12(locals.msgBuffer);
+ digestToOrderHash(locals.digest, locals.entry.orderHash);
+ output.orderHash = locals.entry.orderHash;
+ locals.logMsg.orderHash = locals.entry.orderHash;
+ locals.logMsg.orderEra = locals.entry.orderEra;
+
+ state.mut().lockedOrders.set((uint32)locals.idx, locals.entry);
+ output.success = true;
+ copyFromBuffer(locals.logMsg.to, input.toAddress);
+ locals.logMsg.amount = locals.entry.amount;
+ locals.logMsg.relayerFee = locals.entry.relayerFee;
+ locals.logMsg.networkOut = locals.entry.networkOut;
+ locals.logMsg.success = 1;
+ locals.logMsg.reasonCode = QSBReasonNone;
+ LOG_INFO(locals.logMsg);
+ }
+
+ // View helpers
+ PUBLIC_FUNCTION(GetConfig)
+ {
+ output.admin = state.get().admin;
+ output.protocolFeeRecipient = state.get().protocolFeeRecipient;
+ output.oracleFeeRecipient = state.get().oracleFeeRecipient;
+ output.bpsFee = state.get().bpsFee;
+ output.protocolFee = state.get().protocolFee;
+ output.oracleCount = state.get().oracleCount;
+ output.pauserCount = state.get().pauserCount;
+ output.oracleThreshold = state.get().oracleThreshold;
+ output.paused = state.get().paused;
+ output.orderEra = state.get().orderEra;
+ }
+
+ PUBLIC_FUNCTION(IsOracle)
+ {
+ output.isOracle = (findOracleIndex(state, input.account, 0) != NULL_INDEX);
+ }
+
+ PUBLIC_FUNCTION(IsPauser)
+ {
+ output.isPauser = (findPauserIndex(state, input.account, 0) != NULL_INDEX);
+ }
+
+ struct GetLockedOrder_locals
+ {
+ sint64 idx;
+ };
+
+ PUBLIC_FUNCTION_WITH_LOCALS(GetLockedOrder)
+ {
+ locals.idx = findLockedOrderIndexByNonce(state, input.nonce, 0);
+ output.exists = (locals.idx != NULL_INDEX);
+ if (output.exists)
+ {
+ output.order = state.get().lockedOrders.get((uint32)locals.idx);
+ }
+ }
+
+ struct IsOrderFilled_locals
+ {
+ FilledOrderEntry entry;
+ bool same;
+ };
+
+ PUBLIC_FUNCTION_WITH_LOCALS(IsOrderFilled)
+ {
+ output.filled = isOrderFilled(state, input.hash, 0, 0, locals.same, locals.entry);
+ }
+
+ struct ComputeOrderHash_locals
+ {
+ id digest;
+ QSBOrderMessage msgBuffer;
+ OrderHash tmpIdBytes;
+ uint32 i;
+ };
+
+ PUBLIC_FUNCTION_WITH_LOCALS(ComputeOrderHash)
+ {
+ buildOrderMessage(locals.msgBuffer, input.order, locals.tmpIdBytes, locals.i);
+ locals.digest = qpi.K12(locals.msgBuffer);
+ output.hash.setMem(locals.digest);
+ }
+
+ struct GetOracles_locals
+ {
+ uint32 i;
+ RoleEntry entry;
+ };
+
+ PUBLIC_FUNCTION_WITH_LOCALS(GetOracles)
+ {
+ output.count = 0;
+ setMemory(output.accounts, 0);
+ for (locals.i = 0; locals.i < state.get().oracles.capacity() && output.count < output.accounts.capacity(); ++locals.i)
+ {
+ locals.entry = state.get().oracles.get(locals.i);
+ if (locals.entry.active)
+ {
+ output.accounts.set(output.count, locals.entry.account);
+ ++output.count;
+ }
+ }
+ }
+
+ struct GetPausers_locals
+ {
+ uint32 i;
+ RoleEntry entry;
+ };
+
+ PUBLIC_FUNCTION_WITH_LOCALS(GetPausers)
+ {
+ output.count = 0;
+ setMemory(output.accounts, 0);
+ for (locals.i = 0; locals.i < state.get().pausers.capacity() && output.count < output.accounts.capacity(); ++locals.i)
+ {
+ locals.entry = state.get().pausers.get(locals.i);
+ if (locals.entry.active)
+ {
+ output.accounts.set(output.count, locals.entry.account);
+ ++output.count;
+ }
+ }
+ }
+
+ struct GetLockedOrders_locals
+ {
+ uint32 i;
+ uint32 totalActive;
+ uint32 collected;
+ uint32 effectiveLimit;
+ LockedOrderEntry entry;
+ };
+
+ PUBLIC_FUNCTION_WITH_LOCALS(GetLockedOrders)
+ {
+ output.totalActive = 0;
+ output.returned = 0;
+ setMemory(output.entries, 0);
+ locals.effectiveLimit = input.limit;
+ if (locals.effectiveLimit > QSB_QUERY_MAX_PAGE_SIZE)
+ locals.effectiveLimit = QSB_QUERY_MAX_PAGE_SIZE;
+ locals.collected = 0;
+ for (locals.i = 0; locals.i < state.get().lockedOrders.capacity(); ++locals.i)
+ {
+ locals.entry = state.get().lockedOrders.get(locals.i);
+ if (!locals.entry.active)
+ continue;
+ ++locals.totalActive;
+ if (locals.totalActive <= input.offset)
+ continue;
+ if (locals.collected >= locals.effectiveLimit)
+ continue;
+ output.entries.set(locals.collected, locals.entry);
+ ++locals.collected;
+ }
+ output.totalActive = locals.totalActive;
+ output.returned = locals.collected;
+ }
+
+ struct GetFilledOrders_locals
+ {
+ uint32 i;
+ uint32 totalActive;
+ uint32 collected;
+ uint32 effectiveLimit;
+ FilledOrderEntry entry;
+ };
+
+ PUBLIC_FUNCTION_WITH_LOCALS(GetFilledOrders)
+ {
+ output.totalActive = 0;
+ output.returned = 0;
+ setMemory(output.hashes, 0);
+ locals.effectiveLimit = input.limit;
+ if (locals.effectiveLimit > QSB_QUERY_MAX_PAGE_SIZE)
+ locals.effectiveLimit = QSB_QUERY_MAX_PAGE_SIZE;
+ locals.collected = 0;
+ for (locals.i = 0; locals.i < state.get().filledOrders.capacity(); ++locals.i)
+ {
+ locals.entry = state.get().filledOrders.get(locals.i);
+ if (!locals.entry.used)
+ continue;
+ ++locals.totalActive;
+ if (locals.totalActive <= input.offset)
+ continue;
+ if (locals.collected >= locals.effectiveLimit)
+ continue;
+ output.hashes.set(locals.collected, locals.entry.hash);
+ ++locals.collected;
+ }
+ output.totalActive = locals.totalActive;
+ output.returned = locals.collected;
+ }
+
+ struct Unlock_locals
+ {
+ id digest;
+ OrderHash hash;
+ QSBOrderMessage msgBuffer;
+ OrderHash tmpIdBytes;
+ uint32 validSignatureCount;
+ uint32 requiredSignatures;
+ FilledOrderEntry entry;
+ Array seenSigners;
+ SignatureData sig;
+ uint32 seenCount;
+ uint32 i;
+ uint32 j;
+ uint64 netAmount;
+ uint128 tmpMul;
+ uint128 tmpMul2;
+ uint64 bpsFeeAmount;
+ uint64 protocolFeeAmount;
+ uint64 oracleFeeAmount;
+ uint64 recipientAmount;
+ bool same;
+ bool allTransfersOk;
+ Entity entity;
+ uint64 contractBalance;
+ QSBLogUnlockMessage logMsg;
+ };
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(Unlock)
+ {
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogUnlock;
+ setMemory(locals.logMsg.orderHash, 0);
+ locals.logMsg.toAddress = input.order.toAddress;
+ locals.logMsg.amount = input.order.amount;
+ locals.logMsg.relayerFee = input.order.relayerFee;
+ locals.logMsg.relayer = qpi.invocator();
+ locals.logMsg.orderEra = input.order.orderEra;
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonNone;
+ locals.logMsg._terminator = 0;
+ output.success = false;
+ setMemory(output.orderHash, 0);
+
+ // Must not be paused
+ if (state.get().paused)
+ {
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+ locals.logMsg.reasonCode = QSBReasonPaused;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ // Refund any invocation reward (relayer is paid from order.amount, not from reward)
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+
+ // Basic order validation
+ if (input.order.amount == 0 || input.order.relayerFee >= input.order.amount)
+ {
+ locals.logMsg.reasonCode = QSBReasonInvalidAmount;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ // Check that the contract has enough balance to cover the full order amount.
+ // This should never fail under normal circumstances (Lock keeps funds inside the contract), but we guard against any unexpected balance discrepancies.
+ qpi.getEntity(SELF, locals.entity);
+ if (locals.entity.incomingAmount < locals.entity.outgoingAmount)
+ {
+ locals.contractBalance = 0;
+ }
+ else
+ {
+ locals.contractBalance = locals.entity.incomingAmount - locals.entity.outgoingAmount;
+ }
+
+ if (locals.contractBalance < input.order.amount)
+ {
+ locals.logMsg.reasonCode = QSBReasonInsufficientReward;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ // Era validation: reject orders whose era does not match the current era.
+ // This prevents replay attacks after the filledOrders ring buffer wraps.
+ if (input.order.orderEra != state.get().orderEra)
+ {
+ locals.logMsg.reasonCode = QSBReasonEraMismatch;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ // NOTE: We intentionally do not require a matching lock() entry here.
+ // Unlock is driven solely by:
+ // - oracle signatures over the burn/unlock order (on the other chain),
+ // - replay protection via filledOrders,
+ // - and balance checks on this contract.
+ // This matches a fungible lock/mint ↔ burn/unlock bridge model where
+ // minted tokens can be freely transferred and aggregated, and where
+ // individual locks are not tied 1:1 to specific unlocks.
+
+ // Serialize order with domain prefix and compute K12 digest
+ buildOrderMessage(locals.msgBuffer, input.order, locals.tmpIdBytes, locals.i);
+ locals.digest = qpi.K12(locals.msgBuffer);
+ digestToOrderHash(locals.digest, locals.hash);
+ output.orderHash = locals.hash;
+ locals.logMsg.orderHash = locals.hash;
+
+ // Ensure orderHash not yet filled
+ if (isOrderFilled(state, locals.hash, 0, 0, 0, locals.entry))
+ {
+ locals.logMsg.reasonCode = QSBReasonAlreadyFilled;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ // Verify oracle signatures against threshold
+ if (state.get().oracleCount == 0 || input.numSignatures == 0)
+ {
+ locals.logMsg.reasonCode = QSBReasonNoOracles;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ // requiredSignatures = ceil(oracleCount * oracleThreshold / 100)
+ locals.tmpMul = uint128(state.get().oracleCount) * uint128(state.get().oracleThreshold);
+ locals.tmpMul2 = div(locals.tmpMul, uint128(100));
+ locals.requiredSignatures = (uint32)locals.tmpMul2.low;
+ if (locals.requiredSignatures * 100 < state.get().oracleCount * state.get().oracleThreshold)
+ {
+ ++locals.requiredSignatures;
+ }
+ if (locals.requiredSignatures == 0)
+ {
+ locals.requiredSignatures = 1;
+ }
+
+ locals.validSignatureCount = 0;
+ locals.seenCount = 0;
+
+ for (locals.i = 0; locals.i < input.numSignatures && locals.i < input.signatures.capacity(); ++locals.i)
+ {
+ locals.sig = input.signatures.get(locals.i);
+
+ // Check signer is authorized oracle
+ if (findOracleIndex(state, locals.sig.signer, 0) == NULL_INDEX)
+ {
+ locals.logMsg.reasonCode = QSBReasonInvalidSignature;
+ LOG_INFO(locals.logMsg); // unknown signer -> fail fast
+ return;
+ }
+
+ // Check duplicates
+ for (locals.j = 0; locals.j < locals.seenCount; ++locals.j)
+ {
+ if (locals.seenSigners.get(locals.j) == locals.sig.signer)
+ {
+ locals.logMsg.reasonCode = QSBReasonDuplicateSigner;
+ LOG_INFO(locals.logMsg); // duplicate signer -> fail
+ return;
+ }
+ }
+
+ // Verify signature
+ if (!qpi.signatureValidity(locals.sig.signer, locals.digest, locals.sig.signature))
+ {
+ locals.logMsg.reasonCode = QSBReasonInvalidSignature;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ // Record signer and increment count
+ if (locals.seenCount < locals.seenSigners.capacity())
+ {
+ locals.seenSigners.set(locals.seenCount, locals.sig.signer);
+ ++locals.seenCount;
+ }
+ ++locals.validSignatureCount;
+ }
+
+ if (locals.validSignatureCount < locals.requiredSignatures)
+ {
+ locals.logMsg.reasonCode = QSBReasonThresholdFailed;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ // -----------------------------------------------------------------
+ // Fee calculations
+ // -----------------------------------------------------------------
+ locals.netAmount = input.order.amount - input.order.relayerFee;
+
+ // bpsFeeAmount = netAmount * bpsFee / 10000
+ locals.tmpMul = uint128(locals.netAmount) * uint128(state.get().bpsFee);
+ locals.tmpMul2 = div(locals.tmpMul, uint128(10000));
+ locals.bpsFeeAmount = (uint64)locals.tmpMul2.low;
+
+ // protocolFeeAmount = bpsFeeAmount * protocolFee / 100
+ locals.tmpMul = uint128(locals.bpsFeeAmount) * uint128(state.get().protocolFee);
+ locals.tmpMul2 = div(locals.tmpMul, uint128(100));
+ locals.protocolFeeAmount = (uint64)locals.tmpMul2.low;
+
+ // oracleFeeAmount = bpsFeeAmount - protocolFeeAmount
+ if (locals.bpsFeeAmount >= locals.protocolFeeAmount)
+ locals.oracleFeeAmount = locals.bpsFeeAmount - locals.protocolFeeAmount;
+ else
+ locals.oracleFeeAmount = 0;
+
+ // recipientAmount = netAmount - bpsFeeAmount
+ if (locals.netAmount >= locals.bpsFeeAmount)
+ locals.recipientAmount = locals.netAmount - locals.bpsFeeAmount;
+ else
+ locals.recipientAmount = 0;
+
+ // -----------------------------------------------------------------
+ // Mark order as filled BEFORE transfers to prevent replay.
+ // If a transfer fails below, the order stays filled (no double-pay).
+ // The balance check above guarantees the contract has enough funds.
+ // -----------------------------------------------------------------
+ markOrderFilled(state, locals.hash, 0, 0, 0, locals.entry);
+
+ // -----------------------------------------------------------------
+ // Token transfers
+ // -----------------------------------------------------------------
+
+ locals.allTransfersOk = true;
+
+ // Recipient payout first (most important transfer)
+ if (locals.recipientAmount > 0 && !isZero(input.order.toAddress))
+ {
+ if (qpi.transfer(input.order.toAddress, (sint64)locals.recipientAmount) < 0)
+ {
+ locals.allTransfersOk = false;
+ }
+ }
+
+ // Relayer fee to caller
+ if (input.order.relayerFee > 0)
+ {
+ if (qpi.transfer(qpi.invocator(), (sint64)input.order.relayerFee) < 0)
+ {
+ locals.allTransfersOk = false;
+ }
+ }
+
+ // Protocol fee
+ if (locals.protocolFeeAmount > 0 && !isZero(state.get().protocolFeeRecipient))
+ {
+ if (qpi.transfer(state.get().protocolFeeRecipient, (sint64)locals.protocolFeeAmount) < 0)
+ {
+ locals.allTransfersOk = false;
+ }
+ }
+
+ // Oracle fee
+ if (locals.oracleFeeAmount > 0 && !isZero(state.get().oracleFeeRecipient))
+ {
+ if (qpi.transfer(state.get().oracleFeeRecipient, (sint64)locals.oracleFeeAmount) < 0)
+ {
+ locals.allTransfersOk = false;
+ }
+ }
+
+ if (!locals.allTransfersOk)
+ {
+ locals.logMsg.reasonCode = QSBReasonTransferFailed;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ output.success = true;
+ locals.logMsg.success = 1;
+ locals.logMsg.reasonCode = QSBReasonNone;
+ LOG_INFO(locals.logMsg);
+ }
+
+ // ---------------------------------------------------------------------
+ // Admin procedures
+ // ---------------------------------------------------------------------
+
+ struct TransferAdmin_locals
+ {
+ QSBLogAdminTransferredMessage logMsg;
+ };
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(TransferAdmin)
+ {
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogAdminTransferred;
+ locals.logMsg.previousAdmin = state.get().admin;
+ locals.logMsg.newAdmin = input.newAdmin;
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonNone;
+ locals.logMsg._terminator = 0;
+
+ output.success = false;
+
+ // Refund any attached funds
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+
+ if (!isAdmin(state, qpi.invocator()))
+ {
+ locals.logMsg.reasonCode = QSBReasonNotAdmin;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ state.mut().admin = input.newAdmin;
+ output.success = true;
+ locals.logMsg.success = 1;
+ LOG_INFO(locals.logMsg);
+ }
+
+ struct EditOracleThreshold_locals
+ {
+ QSBLogThresholdUpdatedMessage logMsg;
+ };
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(EditOracleThreshold)
+ {
+ output.success = false;
+ output.oldThreshold = state.get().oracleThreshold;
+
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+
+ if (!isAdmin(state, qpi.invocator()))
+ {
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogThresholdUpdated;
+ locals.logMsg.oldThreshold = output.oldThreshold;
+ locals.logMsg.newThreshold = input.newThreshold;
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonNotAdmin;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ if (input.newThreshold == 0 || input.newThreshold > 100)
+ {
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogThresholdUpdated;
+ locals.logMsg.oldThreshold = output.oldThreshold;
+ locals.logMsg.newThreshold = input.newThreshold;
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonInvalidThreshold;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ state.mut().oracleThreshold = input.newThreshold;
+ output.success = true;
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogThresholdUpdated;
+ locals.logMsg.oldThreshold = output.oldThreshold;
+ locals.logMsg.newThreshold = input.newThreshold;
+ locals.logMsg.success = 1;
+ locals.logMsg.reasonCode = QSBReasonNone;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ }
+
+ struct AddRole_locals
+ {
+ RoleEntry entry;
+ uint32 i;
+ QSBLogRoleMessage logMsg;
+ };
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(AddRole)
+ {
+ output.success = false;
+
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+
+ if (!isAdmin(state, qpi.invocator()))
+ {
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogRoleGranted;
+ locals.logMsg.role = input.role;
+ locals.logMsg.account = input.account;
+ locals.logMsg.caller = qpi.invocator();
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonNotAdmin;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ if (input.role == (uint8)Role::Oracle)
+ {
+ if (findOracleIndex(state, input.account, 0) != NULL_INDEX)
+ {
+ output.success = true;
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogRoleGranted;
+ locals.logMsg.role = input.role;
+ locals.logMsg.account = input.account;
+ locals.logMsg.caller = qpi.invocator();
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonRoleExists;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ for (locals.i = 0; locals.i < state.get().oracles.capacity(); ++locals.i)
+ {
+ locals.entry = state.get().oracles.get(locals.i);
+ if (!locals.entry.active)
+ {
+ locals.entry.account = input.account;
+ locals.entry.active = true;
+ state.mut().oracles.set(locals.i, locals.entry);
+ ++state.mut().oracleCount;
+ output.success = true;
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogRoleGranted;
+ locals.logMsg.role = input.role;
+ locals.logMsg.account = input.account;
+ locals.logMsg.caller = qpi.invocator();
+ locals.logMsg.success = 1;
+ locals.logMsg.reasonCode = QSBReasonNone;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+ }
+ }
+ else if (input.role == (uint8)Role::Pauser)
+ {
+ if (findPauserIndex(state, input.account, 0) != NULL_INDEX)
+ {
+ output.success = true;
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogRoleGranted;
+ locals.logMsg.role = input.role;
+ locals.logMsg.account = input.account;
+ locals.logMsg.caller = qpi.invocator();
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonRoleExists;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ for (locals.i = 0; locals.i < state.get().pausers.capacity(); ++locals.i)
+ {
+ locals.entry = state.get().pausers.get(locals.i);
+ if (!locals.entry.active)
+ {
+ locals.entry.account = input.account;
+ locals.entry.active = true;
+ state.mut().pausers.set(locals.i, locals.entry);
+ ++state.mut().pauserCount;
+ output.success = true;
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogRoleGranted;
+ locals.logMsg.role = input.role;
+ locals.logMsg.account = input.account;
+ locals.logMsg.caller = qpi.invocator();
+ locals.logMsg.success = 1;
+ locals.logMsg.reasonCode = QSBReasonNone;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+ }
+ }
+ }
+
+ struct RemoveRole_locals
+ {
+ RoleEntry entry;
+ sint64 idx;
+ QSBLogRoleMessage logMsg;
+ };
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(RemoveRole)
+ {
+ output.success = false;
+
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+
+ if (!isAdmin(state, qpi.invocator()))
+ {
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogRoleRevoked;
+ locals.logMsg.role = input.role;
+ locals.logMsg.account = input.account;
+ locals.logMsg.caller = qpi.invocator();
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonNotAdmin;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ if (input.role == (uint8)Role::Oracle)
+ {
+ locals.idx = findOracleIndex(state, input.account, 0);
+ if (locals.idx != NULL_INDEX)
+ {
+ locals.entry = state.get().oracles.get((uint32)locals.idx);
+ locals.entry.active = false;
+ state.mut().oracles.set((uint32)locals.idx, locals.entry);
+ if (state.get().oracleCount > 0)
+ --state.mut().oracleCount;
+ output.success = true;
+
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogRoleRevoked;
+ locals.logMsg.role = input.role;
+ locals.logMsg.account = input.account;
+ locals.logMsg.caller = qpi.invocator();
+ locals.logMsg.success = 1;
+ locals.logMsg.reasonCode = QSBReasonNone;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ }
+ else
+ {
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogRoleRevoked;
+ locals.logMsg.role = input.role;
+ locals.logMsg.account = input.account;
+ locals.logMsg.caller = qpi.invocator();
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonRoleMissing;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ }
+ }
+ else if (input.role == (uint8)Role::Pauser)
+ {
+ locals.idx = findPauserIndex(state, input.account, 0);
+ if (locals.idx != NULL_INDEX)
+ {
+ locals.entry = state.get().pausers.get((uint32)locals.idx);
+ locals.entry.active = false;
+ state.mut().pausers.set((uint32)locals.idx, locals.entry);
+ if (state.get().pauserCount > 0)
+ --state.mut().pauserCount;
+ output.success = true;
+
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogRoleRevoked;
+ locals.logMsg.role = input.role;
+ locals.logMsg.account = input.account;
+ locals.logMsg.caller = qpi.invocator();
+ locals.logMsg.success = 1;
+ locals.logMsg.reasonCode = QSBReasonNone;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ }
+ else
+ {
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogRoleRevoked;
+ locals.logMsg.role = input.role;
+ locals.logMsg.account = input.account;
+ locals.logMsg.caller = qpi.invocator();
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonRoleMissing;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ }
+ }
+ }
+
+ struct Pause_locals
+ {
+ QSBLogPausedMessage logMsg;
+ };
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(Pause)
+ {
+ output.success = false;
+
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+
+ if (!isAdminOrPauser(state, qpi.invocator(), 0))
+ {
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogPaused;
+ locals.logMsg.caller = qpi.invocator();
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonNotAdminOrPauser;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ state.mut().paused = true;
+ output.success = true;
+
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogPaused;
+ locals.logMsg.caller = qpi.invocator();
+ locals.logMsg.success = 1;
+ locals.logMsg.reasonCode = QSBReasonNone;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ }
+
+ struct Unpause_locals
+ {
+ QSBLogPausedMessage logMsg;
+ };
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(Unpause)
+ {
+ output.success = false;
+
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+
+ if (!isAdminOrPauser(state, qpi.invocator(), 0))
+ {
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogUnpaused;
+ locals.logMsg.caller = qpi.invocator();
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonNotAdminOrPauser;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ state.mut().paused = false;
+ output.success = true;
+
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogUnpaused;
+ locals.logMsg.caller = qpi.invocator();
+ locals.logMsg.success = 1;
+ locals.logMsg.reasonCode = QSBReasonNone;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ }
+
+ struct EditFeeParameters_locals
+ {
+ QSBLogFeeParametersUpdatedMessage logMsg;
+ };
+
+ PUBLIC_PROCEDURE_WITH_LOCALS(EditFeeParameters)
+ {
+ output.success = false;
+
+ if (qpi.invocationReward() > 0)
+ {
+ qpi.transfer(qpi.invocator(), qpi.invocationReward());
+ }
+
+ if (!isAdmin(state, qpi.invocator()))
+ {
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogFeeParametersUpdated;
+ locals.logMsg.bpsFee = state.get().bpsFee;
+ locals.logMsg.protocolFee = state.get().protocolFee;
+ locals.logMsg.protocolFeeRecipient = state.get().protocolFeeRecipient;
+ locals.logMsg.oracleFeeRecipient = state.get().oracleFeeRecipient;
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonNotAdmin;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ // Validate fee ranges (when non-zero values are provided)
+ if (input.bpsFee != 0 && input.bpsFee > QSB_MAX_BPS_FEE)
+ {
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogFeeParametersUpdated;
+ locals.logMsg.bpsFee = state.get().bpsFee;
+ locals.logMsg.protocolFee = state.get().protocolFee;
+ locals.logMsg.protocolFeeRecipient = state.get().protocolFeeRecipient;
+ locals.logMsg.oracleFeeRecipient = state.get().oracleFeeRecipient;
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonInvalidFeeParams;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ if (input.protocolFee != 0 && input.protocolFee > QSB_MAX_PROTOCOL_FEE)
+ {
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogFeeParametersUpdated;
+ locals.logMsg.bpsFee = state.get().bpsFee;
+ locals.logMsg.protocolFee = state.get().protocolFee;
+ locals.logMsg.protocolFeeRecipient = state.get().protocolFeeRecipient;
+ locals.logMsg.oracleFeeRecipient = state.get().oracleFeeRecipient;
+ locals.logMsg.success = 0;
+ locals.logMsg.reasonCode = QSBReasonInvalidFeeParams;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ return;
+ }
+
+ // Only non-zero values are updated
+ if (input.bpsFee != 0)
+ {
+ state.mut().bpsFee = input.bpsFee;
+ }
+
+ if (input.protocolFee != 0)
+ {
+ state.mut().protocolFee = input.protocolFee;
+ }
+
+ if (!isZero(input.protocolFeeRecipient))
+ {
+ state.mut().protocolFeeRecipient = input.protocolFeeRecipient;
+ }
+
+ if (!isZero(input.oracleFeeRecipient))
+ {
+ state.mut().oracleFeeRecipient = input.oracleFeeRecipient;
+ }
+
+ output.success = true;
+
+ locals.logMsg._contractIndex = SELF_INDEX;
+ locals.logMsg._type = QSBLogFeeParametersUpdated;
+ locals.logMsg.bpsFee = state.get().bpsFee;
+ locals.logMsg.protocolFee = state.get().protocolFee;
+ locals.logMsg.protocolFeeRecipient = state.get().protocolFeeRecipient;
+ locals.logMsg.oracleFeeRecipient = state.get().oracleFeeRecipient;
+ locals.logMsg.success = 1;
+ locals.logMsg.reasonCode = QSBReasonNone;
+ locals.logMsg._terminator = 0;
+ LOG_INFO(locals.logMsg);
+ }
+
+ REGISTER_USER_FUNCTIONS_AND_PROCEDURES()
+ {
+ // View functions
+ REGISTER_USER_FUNCTION(GetConfig, 1);
+ REGISTER_USER_FUNCTION(IsOracle, 2);
+ REGISTER_USER_FUNCTION(IsPauser, 3);
+ REGISTER_USER_FUNCTION(GetLockedOrder, 4);
+ REGISTER_USER_FUNCTION(IsOrderFilled, 5);
+ REGISTER_USER_FUNCTION(ComputeOrderHash, 6);
+ REGISTER_USER_FUNCTION(GetOracles, 7);
+ REGISTER_USER_FUNCTION(GetPausers, 8);
+ REGISTER_USER_FUNCTION(GetLockedOrders, 9);
+ REGISTER_USER_FUNCTION(GetFilledOrders, 10);
+
+ // User procedures
+ REGISTER_USER_PROCEDURE(Lock, 1);
+ REGISTER_USER_PROCEDURE(OverrideLock, 2);
+ REGISTER_USER_PROCEDURE(Unlock, 3);
+
+ // Admin procedures
+ REGISTER_USER_PROCEDURE(TransferAdmin, 10);
+ REGISTER_USER_PROCEDURE(EditOracleThreshold, 11);
+ REGISTER_USER_PROCEDURE(AddRole, 12);
+ REGISTER_USER_PROCEDURE(RemoveRole, 13);
+ REGISTER_USER_PROCEDURE(Pause, 14);
+ REGISTER_USER_PROCEDURE(Unpause, 15);
+ REGISTER_USER_PROCEDURE(EditFeeParameters, 16);
+ }
+
+ // ---------------------------------------------------------------------
+ // Epoch processing
+ // ---------------------------------------------------------------------
+
+ struct END_EPOCH_locals
+ {
+ // No periodic processing required in the current bridge design.
+ };
+
+ END_EPOCH_WITH_LOCALS()
+ {
+ // Intentionally left empty.
+ }
+
+ // ---------------------------------------------------------------------
+ // Initialization
+ // ---------------------------------------------------------------------
+
+ INITIALIZE()
+ {
+ // No admin set initially; first TransferAdmin call bootstraps admin.
+ state.mut().admin = id(100, 200, 300, 400);
+ state.mut().paused = false;
+
+ state.mut().oracleThreshold = 67; // default 67% (2/3 + 1 style)
+ state.mut().lastLockedOrdersNextOverwriteIdx = 0;
+ state.mut().lastFilledOrdersNextOverwriteIdx = 0;
+ state.mut().oracleCount = 0;
+ state.mut().pauserCount = 0;
+
+ // Clear role mappings and filled order table
+ setMemory(state.mut().oracles, 0);
+ setMemory(state.mut().pausers, 0);
+ setMemory(state.mut().filledOrders, 0);
+ setMemory(state.mut().lockedOrders, 0);
+
+ // Default fee configuration: no fees(it will be decided later)
+ state.mut().bpsFee = 0;
+ state.mut().protocolFee = 0;
+ state.mut().protocolFeeRecipient = NULL_ID;
+ state.mut().oracleFeeRecipient = NULL_ID;
+
+ state.mut().orderEra = 0;
+ }
+};
diff --git a/test/contract_qsb.cpp b/test/contract_qsb.cpp
new file mode 100644
index 00000000..e6121d46
--- /dev/null
+++ b/test/contract_qsb.cpp
@@ -0,0 +1,1509 @@
+#define NO_UEFI
+
+#include "contract_testing.h"
+
+static const id QSB_CONTRACT_ID(QSB_CONTRACT_INDEX, 0, 0, 0);
+static const id USER1(123, 456, 789, 876);
+static const id USER2(42, 424, 4242, 42424);
+static const id ADMIN(100, 200, 300, 400);
+static const id ORACLE1(500, 600, 700, 800);
+static const id ORACLE2(900, 1000, 1100, 1200);
+static const id ORACLE3(1300, 1400, 1500, 1600);
+static const id PAUSER1(1700, 1800, 1900, 2000);
+static const id PROTOCOL_FEE_RECIPIENT(2100, 2200, 2300, 2400);
+static const id ORACLE_FEE_RECIPIENT(2500, 2600, 2700, 2800);
+
+class StateCheckerQSB : public QSB, public QSB::StateData
+{
+public:
+ const QPI::ContractState& asState() const {
+ return *reinterpret_cast*>(static_cast(this));
+ }
+ QPI::ContractState& asMutState() {
+ return *reinterpret_cast*>(static_cast(this));
+ }
+
+ void checkAdmin(const id& expectedAdmin) const
+ {
+ EXPECT_EQ(this->admin, expectedAdmin);
+ }
+
+ void checkPaused(bool expectedPaused) const
+ {
+ EXPECT_EQ((bool)this->paused, expectedPaused);
+ }
+
+ void checkOracleThreshold(uint8 expectedThreshold) const
+ {
+ EXPECT_EQ(this->oracleThreshold, expectedThreshold);
+ }
+
+ void checkOracleCount(uint32 expectedCount) const
+ {
+ EXPECT_EQ(this->oracleCount, expectedCount);
+ }
+
+ void checkBpsFee(uint32 expectedFee) const
+ {
+ EXPECT_EQ(this->bpsFee, expectedFee);
+ }
+
+ void checkProtocolFee(uint32 expectedFee) const
+ {
+ EXPECT_EQ(this->protocolFee, expectedFee);
+ }
+
+ void checkProtocolFeeRecipient(const id& expectedRecipient) const
+ {
+ EXPECT_EQ(this->protocolFeeRecipient, expectedRecipient);
+ }
+
+ void checkOracleFeeRecipient(const id& expectedRecipient) const
+ {
+ EXPECT_EQ(this->oracleFeeRecipient, expectedRecipient);
+ }
+
+ // Helper to mark an order hash as filled via the internal ring buffer logic.
+ void forceMarkOrderFilled(const QSB::OrderHash& hash)
+ {
+ FilledOrderEntry entry;
+ bool same = false;
+ markOrderFilled(asMutState(), hash, 0, 0, same, entry);
+ }
+};
+
+class ContractTestingQSB : protected ContractTesting
+{
+public:
+ ContractTestingQSB()
+ {
+ initEmptySpectrum();
+ initEmptyUniverse();
+ INIT_CONTRACT(QSB);
+ callSystemProcedure(QSB_CONTRACT_INDEX, INITIALIZE);
+
+ checkContractExecCleanup();
+ }
+
+ ~ContractTestingQSB()
+ {
+ checkContractExecCleanup();
+ }
+
+ StateCheckerQSB* getState()
+ {
+ return (StateCheckerQSB*)contractStates[QSB_CONTRACT_INDEX];
+ }
+
+ const StateCheckerQSB* getState() const
+ {
+ return (const StateCheckerQSB*)contractStates[QSB_CONTRACT_INDEX];
+ }
+
+ static QSB::Order createTestOrder(
+ const id& fromAddress,
+ const id& toAddress,
+ uint64 amount,
+ uint64 relayerFee,
+ const Array& nonce32,
+ uint32 orderEra = 0)
+ {
+ QSB::Order order;
+ order.fromAddress = fromAddress;
+ order.toAddress = toAddress;
+ setMemory(order.tokenIn, 0);
+ setMemory(order.tokenOut, 0);
+ order.amount = amount;
+ order.relayerFee = relayerFee;
+ order.networkIn = 2;
+ order.networkOut = 1;
+ order.nonce = nonce32;
+ order.orderEra = orderEra;
+ return order;
+ }
+
+ static QSB::Order createTestOrderFromU32Nonce(
+ const id& fromAddress,
+ const id& toAddress,
+ uint64 amount,
+ uint64 relayerFee,
+ uint32 nonce,
+ uint32 orderEra = 0)
+ {
+ Array nonce32;
+ setMemory(nonce32, 0);
+ nonce32.set(0, (uint8)(nonce & 0xFF));
+ nonce32.set(1, (uint8)((nonce >> 8) & 0xFF));
+ nonce32.set(2, (uint8)((nonce >> 16) & 0xFF));
+ nonce32.set(3, (uint8)((nonce >> 24) & 0xFF));
+ return createTestOrder(fromAddress, toAddress, amount, relayerFee, nonce32, orderEra);
+ }
+
+ // Helper to create signature data (mock - in real tests would need actual signatures)
+ QSB::SignatureData createMockSignature(const id& signer) const
+ {
+ QSB::SignatureData sig;
+ sig.signer = signer;
+ // In real implementation, this would be a valid signature
+ // For testing, we'll use zeros (signature validation will fail, but structure is correct)
+ setMemory(sig.signature, 0);
+ return sig;
+ }
+
+ // Helper to create a zero-initialized address array
+ static Array createZeroAddress()
+ {
+ Array addr;
+ setMemory(addr, 0);
+ return addr;
+ }
+
+ // ============================================================================
+ // User Procedure Helpers
+ // ============================================================================
+
+ QSB::Lock_output lock(const id& user, uint64 amount, uint64 relayerFee, uint32 networkOut, uint32 nonce, const Array& toAddress, uint64 energyAmount)
+ {
+ QSB::Lock_input input;
+ QSB::Lock_output output;
+
+ input.amount = amount;
+ input.relayerFee = relayerFee;
+ input.networkOut = networkOut;
+ input.nonce = nonce;
+ copyToBuffer(input.toAddress, toAddress, true);
+
+ invokeUserProcedure(QSB_CONTRACT_INDEX, 1, input, output, user, energyAmount);
+ return output;
+ }
+
+ QSB::OverrideLock_output overrideLock(const id& user, uint32 nonce, uint64 relayerFee, const Array& toAddress)
+ {
+ QSB::OverrideLock_input input;
+ QSB::OverrideLock_output output;
+
+ input.nonce = nonce;
+ input.relayerFee = relayerFee;
+ copyToBuffer(input.toAddress, toAddress, true);
+
+ invokeUserProcedure(QSB_CONTRACT_INDEX, 2, input, output, user, 0);
+ return output;
+ }
+
+ QSB::Unlock_output unlock(const id& user, const QSB::Order& order, uint32 numSignatures, const Array& signatures)
+ {
+ QSB::Unlock_input input;
+ QSB::Unlock_output output;
+
+ input.order = order;
+ input.numSignatures = numSignatures;
+ copyMemory(input.signatures, signatures);
+
+ invokeUserProcedure(QSB_CONTRACT_INDEX, 3, input, output, user, 0);
+ return output;
+ }
+
+ // ============================================================================
+ // Admin Procedure Helpers
+ // ============================================================================
+
+ QSB::TransferAdmin_output transferAdmin(const id& user, const id& newAdmin)
+ {
+ QSB::TransferAdmin_input input;
+ QSB::TransferAdmin_output output;
+
+ input.newAdmin = newAdmin;
+
+ invokeUserProcedure(QSB_CONTRACT_INDEX, 10, input, output, user, 0);
+ return output;
+ }
+
+ QSB::EditOracleThreshold_output editOracleThreshold(const id& user, uint8 newThreshold)
+ {
+ QSB::EditOracleThreshold_input input;
+ QSB::EditOracleThreshold_output output;
+
+ input.newThreshold = newThreshold;
+
+ invokeUserProcedure(QSB_CONTRACT_INDEX, 11, input, output, user, 0);
+ return output;
+ }
+
+ QSB::AddRole_output addRole(const id& user, uint8 role, const id& account)
+ {
+ QSB::AddRole_input input;
+ QSB::AddRole_output output;
+
+ input.role = role;
+ input.account = account;
+
+ invokeUserProcedure(QSB_CONTRACT_INDEX, 12, input, output, user, 0);
+ return output;
+ }
+
+ QSB::RemoveRole_output removeRole(const id& user, uint8 role, const id& account)
+ {
+ QSB::RemoveRole_input input;
+ QSB::RemoveRole_output output;
+
+ input.role = role;
+ input.account = account;
+
+ invokeUserProcedure(QSB_CONTRACT_INDEX, 13, input, output, user, 0);
+ return output;
+ }
+
+ QSB::Pause_output pause(const id& user)
+ {
+ QSB::Pause_input input;
+ QSB::Pause_output output;
+
+ invokeUserProcedure(QSB_CONTRACT_INDEX, 14, input, output, user, 0);
+ return output;
+ }
+
+ QSB::Unpause_output unpause(const id& user)
+ {
+ QSB::Unpause_input input;
+ QSB::Unpause_output output;
+
+ invokeUserProcedure(QSB_CONTRACT_INDEX, 15, input, output, user, 0);
+ return output;
+ }
+
+ QSB::EditFeeParameters_output editFeeParameters(
+ const id& user,
+ uint32 bpsFee,
+ uint32 protocolFee,
+ const id& protocolFeeRecipient,
+ const id& oracleFeeRecipient)
+ {
+ QSB::EditFeeParameters_input input;
+ QSB::EditFeeParameters_output output;
+
+ input.bpsFee = bpsFee;
+ input.protocolFee = protocolFee;
+ input.protocolFeeRecipient = protocolFeeRecipient;
+ input.oracleFeeRecipient = oracleFeeRecipient;
+
+ invokeUserProcedure(QSB_CONTRACT_INDEX, 16, input, output, user, 0);
+ return output;
+ }
+
+ // ============================================================================
+ // View / helper function wrappers (GetConfig, IsOracle, IsPauser, GetLockedOrder, IsOrderFilled)
+ // ============================================================================
+
+ void runEndEpoch()
+ {
+ callSystemProcedure(QSB_CONTRACT_INDEX, END_EPOCH);
+ }
+
+ QSB::GetConfig_output getConfig() const
+ {
+ QSB::GetConfig_input input;
+ QSB::GetConfig_output output;
+ callFunction(QSB_CONTRACT_INDEX, 1, input, output);
+ return output;
+ }
+
+ QSB::IsOracle_output isOracle(const id& account) const
+ {
+ QSB::IsOracle_input input;
+ QSB::IsOracle_output output;
+ input.account = account;
+ callFunction(QSB_CONTRACT_INDEX, 2, input, output);
+ return output;
+ }
+
+ QSB::IsPauser_output isPauser(const id& account) const
+ {
+ QSB::IsPauser_input input;
+ QSB::IsPauser_output output;
+ input.account = account;
+ callFunction(QSB_CONTRACT_INDEX, 3, input, output);
+ return output;
+ }
+
+ QSB::GetLockedOrder_output getLockedOrder(uint32 nonce) const
+ {
+ QSB::GetLockedOrder_input input;
+ QSB::GetLockedOrder_output output;
+ input.nonce = nonce;
+ callFunction(QSB_CONTRACT_INDEX, 4, input, output);
+ return output;
+ }
+
+ QSB::IsOrderFilled_output isOrderFilled(const QSB::OrderHash& hash) const
+ {
+ QSB::IsOrderFilled_input input;
+ QSB::IsOrderFilled_output output;
+ for (uint32 i = 0; i < input.hash.capacity(); ++i)
+ input.hash.set(i, hash.get(i));
+ callFunction(QSB_CONTRACT_INDEX, 5, input, output);
+ return output;
+ }
+
+ QSB::ComputeOrderHash_output computeOrderHash(const QSB::Order& order) const
+ {
+ QSB::ComputeOrderHash_input input;
+ QSB::ComputeOrderHash_output output;
+ input.order = order;
+ callFunction(QSB_CONTRACT_INDEX, 6, input, output);
+ return output;
+ }
+
+ QSB::GetOracles_output getOracles() const
+ {
+ QSB::GetOracles_input input;
+ QSB::GetOracles_output output;
+ callFunction(QSB_CONTRACT_INDEX, 7, input, output);
+ return output;
+ }
+
+ QSB::GetPausers_output getPausers() const
+ {
+ QSB::GetPausers_input input;
+ QSB::GetPausers_output output;
+ callFunction(QSB_CONTRACT_INDEX, 8, input, output);
+ return output;
+ }
+
+ QSB::GetLockedOrders_output getLockedOrders(uint32 offset, uint32 limit) const
+ {
+ QSB::GetLockedOrders_input input;
+ QSB::GetLockedOrders_output output;
+ input.offset = offset;
+ input.limit = limit;
+ callFunction(QSB_CONTRACT_INDEX, 9, input, output);
+ return output;
+ }
+
+ QSB::GetFilledOrders_output getFilledOrders(uint32 offset, uint32 limit) const
+ {
+ QSB::GetFilledOrders_input input;
+ QSB::GetFilledOrders_output output;
+ input.offset = offset;
+ input.limit = limit;
+ callFunction(QSB_CONTRACT_INDEX, 10, input, output);
+ return output;
+ }
+};
+
+// ============================================================================
+// View helper function tests (GetConfig, IsOracle, IsPauser, GetLockedOrder, IsOrderFilled)
+// ============================================================================
+
+TEST(ContractTestingQSB, TestGetConfig_ReturnsInitialState)
+{
+ ContractTestingQSB test;
+
+ QSB::GetConfig_output config = test.getConfig();
+
+ EXPECT_EQ(config.admin, ADMIN);
+ EXPECT_EQ(config.protocolFeeRecipient, NULL_ID);
+ EXPECT_EQ(config.oracleFeeRecipient, NULL_ID);
+ EXPECT_EQ(config.bpsFee, 0u);
+ EXPECT_EQ(config.protocolFee, 0u);
+ EXPECT_EQ(config.oracleCount, 0u);
+ EXPECT_EQ(config.oracleThreshold, 67);
+ EXPECT_EQ((bool)config.paused, false);
+}
+
+TEST(ContractTestingQSB, TestGetConfig_ReflectsAdminAndFeeChanges)
+{
+ ContractTestingQSB test;
+
+ increaseEnergy(ADMIN, 1);
+ test.editFeeParameters(ADMIN, 50, 20, PROTOCOL_FEE_RECIPIENT, ORACLE_FEE_RECIPIENT);
+
+ QSB::GetConfig_output config = test.getConfig();
+
+ EXPECT_EQ(config.admin, ADMIN);
+ EXPECT_EQ(config.bpsFee, 50u);
+ EXPECT_EQ(config.protocolFee, 20u);
+ EXPECT_EQ(config.protocolFeeRecipient, PROTOCOL_FEE_RECIPIENT);
+ EXPECT_EQ(config.oracleFeeRecipient, ORACLE_FEE_RECIPIENT);
+}
+
+TEST(ContractTestingQSB, TestIsOracle_ReturnsFalseWhenNotOracle)
+{
+ ContractTestingQSB test;
+
+ QSB::IsOracle_output out = test.isOracle(ORACLE1);
+ EXPECT_FALSE((bool)out.isOracle);
+
+ out = test.isOracle(USER1);
+ EXPECT_FALSE((bool)out.isOracle);
+}
+
+TEST(ContractTestingQSB, TestIsOracle_ReturnsTrueAfterAddRole)
+{
+ ContractTestingQSB test;
+
+ increaseEnergy(ADMIN, 1);
+ increaseEnergy(ORACLE1, 1);
+ test.addRole(ADMIN, (uint8)QSB::Role::Oracle, ORACLE1);
+
+ QSB::IsOracle_output out = test.isOracle(ORACLE1);
+ EXPECT_TRUE((bool)out.isOracle);
+
+ out = test.isOracle(ORACLE2);
+ EXPECT_FALSE((bool)out.isOracle);
+}
+
+TEST(ContractTestingQSB, TestIsPauser_ReturnsFalseWhenNotPauser)
+{
+ ContractTestingQSB test;
+
+ increaseEnergy(ADMIN, 1);
+ increaseEnergy(PAUSER1, 1);
+ test.addRole(ADMIN, (uint8)QSB::Role::Pauser, PAUSER1);
+
+ QSB::IsPauser_output out = test.isPauser(PAUSER1);
+ EXPECT_TRUE((bool)out.isPauser);
+
+ out = test.isPauser(ORACLE1);
+ EXPECT_FALSE((bool)out.isPauser);
+}
+
+TEST(ContractTestingQSB, TestIsPauser_ReturnsTrueAfterAddRole)
+{
+ ContractTestingQSB test;
+
+ increaseEnergy(ADMIN, 1);
+ increaseEnergy(PAUSER1, 1);
+ test.addRole(ADMIN, (uint8)QSB::Role::Pauser, PAUSER1);
+
+ QSB::IsPauser_output out = test.isPauser(PAUSER1);
+ EXPECT_TRUE((bool)out.isPauser);
+
+ out = test.isPauser(USER1);
+ EXPECT_FALSE((bool)out.isPauser);
+}
+
+TEST(ContractTestingQSB, TestGetLockedOrder_ReturnsNotExistsForUnknownNonce)
+{
+ ContractTestingQSB test;
+
+ QSB::GetLockedOrder_output out = test.getLockedOrder(999);
+ EXPECT_FALSE((bool)out.exists);
+}
+
+TEST(ContractTestingQSB, TestGetLockedOrder_ReturnsOrderAfterLock)
+{
+ ContractTestingQSB test;
+
+ const uint64 amount = 1000000;
+ const uint64 relayerFee = 10000;
+ const uint32 nonce = 42;
+
+ increaseEnergy(USER1, amount);
+ test.lock(USER1, amount, relayerFee, 1, nonce, ContractTestingQSB::createZeroAddress(), amount);
+
+ QSB::GetLockedOrder_output out = test.getLockedOrder(nonce);
+ EXPECT_TRUE((bool)out.exists);
+ EXPECT_TRUE(out.order.active);
+ EXPECT_EQ(out.order.sender, USER1);
+ EXPECT_EQ(out.order.amount, amount);
+ EXPECT_EQ(out.order.relayerFee, relayerFee);
+ EXPECT_EQ(out.order.nonce, nonce);
+}
+
+TEST(ContractTestingQSB, TestIsOrderFilled_ReturnsFalseForUnknownHash)
+{
+ ContractTestingQSB test;
+
+ QSB::OrderHash unknownHash;
+ for (uint32 i = 0; i < unknownHash.capacity(); ++i)
+ unknownHash.set(i, (uint8)(i & 0xff));
+
+ QSB::IsOrderFilled_output out = test.isOrderFilled(unknownHash);
+ EXPECT_FALSE((bool)out.filled);
+
+ // After marking the hash as filled via the internal helper, it should report true.
+ test.getState()->forceMarkOrderFilled(unknownHash);
+ QSB::IsOrderFilled_output out2 = test.isOrderFilled(unknownHash);
+ EXPECT_TRUE((bool)out2.filled);
+}
+
+// ============================================================================
+// New query function tests (ComputeOrderHash, GetOracles, GetPausers, GetLockedOrders, GetFilledOrders)
+// ============================================================================
+
+TEST(ContractTestingQSB, TestComputeOrderHash_ReturnsConsistentHash)
+{
+ ContractTestingQSB test;
+
+ QSB::Order order = ContractTestingQSB::createTestOrderFromU32Nonce(USER1, USER2, 1000000, 10000, 99);
+ QSB::ComputeOrderHash_output out = test.computeOrderHash(order);
+
+ // Hash should be non-zero
+ bool hashNonZero = false;
+ for (uint32 i = 0; i < out.hash.capacity(); ++i)
+ {
+ if (out.hash.get(i) != 0)
+ {
+ hashNonZero = true;
+ break;
+ }
+ }
+ EXPECT_TRUE(hashNonZero);
+
+ // Same order should produce same hash
+ QSB::ComputeOrderHash_output out2 = test.computeOrderHash(order);
+ for (uint32 i = 0; i < out.hash.capacity(); ++i)
+ EXPECT_EQ(out.hash.get(i), out2.hash.get(i));
+}
+
+TEST(ContractTestingQSB, TestComputeOrderHash_MatchesLockOutput)
+{
+ ContractTestingQSB test;
+
+ const uint64 amount = 1000000;
+ const uint64 relayerFee = 10000;
+ const uint32 nonce = 50;
+
+ increaseEnergy(USER1, amount);
+ QSB::Lock_output lockOut = test.lock(USER1, amount, relayerFee, 1, nonce, ContractTestingQSB::createZeroAddress(), amount);
+ EXPECT_TRUE(lockOut.success);
+
+ QSB::Order order;
+ order.fromAddress = USER1;
+ order.toAddress = NULL_ID;
+ setMemory(order.tokenIn, 0);
+ setMemory(order.tokenOut, 0);
+ order.amount = amount;
+ order.relayerFee = relayerFee;
+ order.networkIn = 1;
+ order.networkOut = 1;
+ setMemory(order.nonce, 0);
+ order.nonce.set(0, (uint8)(nonce & 0xFF));
+ order.nonce.set(1, (uint8)((nonce >> 8) & 0xFF));
+ order.nonce.set(2, (uint8)((nonce >> 16) & 0xFF));
+ order.nonce.set(3, (uint8)((nonce >> 24) & 0xFF));
+ order.orderEra = 0;
+
+ QSB::ComputeOrderHash_output computed = test.computeOrderHash(order);
+ for (uint32 i = 0; i < lockOut.orderHash.capacity(); ++i)
+ EXPECT_EQ(lockOut.orderHash.get(i), computed.hash.get(i));
+}
+
+TEST(ContractTestingQSB, TestGetOracles_ReturnsEmptyWhenNoOracles)
+{
+ ContractTestingQSB test;
+
+ QSB::GetOracles_output out = test.getOracles();
+ EXPECT_EQ(out.count, 0u);
+}
+
+TEST(ContractTestingQSB, TestGetOracles_ReturnsAllOraclesAfterAddRole)
+{
+ ContractTestingQSB test;
+
+ increaseEnergy(ADMIN, 1);
+ increaseEnergy(ORACLE1, 1);
+ increaseEnergy(ORACLE2, 1);
+ test.addRole(ADMIN, (uint8)QSB::Role::Oracle, ORACLE1);
+ test.addRole(ADMIN, (uint8)QSB::Role::Oracle, ORACLE2);
+
+ QSB::GetOracles_output out = test.getOracles();
+ EXPECT_EQ(out.count, 2u);
+ EXPECT_EQ(out.accounts.get(0), ORACLE1);
+ EXPECT_EQ(out.accounts.get(1), ORACLE2);
+}
+
+TEST(ContractTestingQSB, TestGetPausers_ReturnsEmptyWhenNoPausers)
+{
+ ContractTestingQSB test;
+
+ QSB::GetPausers_output out = test.getPausers();
+ EXPECT_EQ(out.count, 0u);
+}
+
+TEST(ContractTestingQSB, TestGetPausers_ReturnsAllPausersAfterAddRole)
+{
+ ContractTestingQSB test;
+
+ increaseEnergy(ADMIN, 1);
+ increaseEnergy(PAUSER1, 1);
+ test.addRole(ADMIN, (uint8)QSB::Role::Pauser, PAUSER1);
+
+ QSB::GetPausers_output out = test.getPausers();
+ EXPECT_EQ(out.count, 1u);
+ EXPECT_EQ(out.accounts.get(0), PAUSER1);
+}
+
+TEST(ContractTestingQSB, TestGetLockedOrders_ReturnsEmptyWhenNoLocks)
+{
+ ContractTestingQSB test;
+
+ QSB::GetLockedOrders_output out = test.getLockedOrders(0, 64);
+ EXPECT_EQ(out.totalActive, 0u);
+ EXPECT_EQ(out.returned, 0u);
+}
+
+TEST(ContractTestingQSB, TestGetLockedOrders_ReturnsLockedOrdersAfterLock)
+{
+ ContractTestingQSB test;
+
+ const uint64 amount = 1000000;
+ const uint64 relayerFee = 10000;
+ const uint32 nonce = 77;
+
+ increaseEnergy(USER1, amount);
+ test.lock(USER1, amount, relayerFee, 1, nonce, ContractTestingQSB::createZeroAddress(), amount);
+
+ QSB::GetLockedOrders_output out = test.getLockedOrders(0, 64);
+ EXPECT_EQ(out.totalActive, 1u);
+ EXPECT_EQ(out.returned, 1u);
+ EXPECT_TRUE(out.entries.get(0).active);
+ EXPECT_EQ(out.entries.get(0).sender, USER1);
+ EXPECT_EQ(out.entries.get(0).amount, amount);
+ EXPECT_EQ(out.entries.get(0).nonce, nonce);
+}
+
+TEST(ContractTestingQSB, TestGetLockedOrders_Pagination)
+{
+ ContractTestingQSB test;
+
+ const uint64 amount = 1;
+ increaseEnergy(USER1, amount * 5);
+
+ for (uint32 i = 0; i < 5; ++i)
+ {
+ test.lock(USER1, amount, 0, 1, i, ContractTestingQSB::createZeroAddress(), amount);
+ }
+
+ QSB::GetLockedOrders_output out = test.getLockedOrders(0, 2);
+ EXPECT_EQ(out.totalActive, 5u);
+ EXPECT_EQ(out.returned, 2u);
+
+ out = test.getLockedOrders(2, 2);
+ EXPECT_EQ(out.totalActive, 5u);
+ EXPECT_EQ(out.returned, 2u);
+
+ out = test.getLockedOrders(4, 2);
+ EXPECT_EQ(out.totalActive, 5u);
+ EXPECT_EQ(out.returned, 1u);
+}
+
+TEST(ContractTestingQSB, TestGetFilledOrders_ReturnsEmptyWhenNoFills)
+{
+ ContractTestingQSB test;
+
+ QSB::GetFilledOrders_output out = test.getFilledOrders(0, 64);
+ EXPECT_EQ(out.totalActive, 0u);
+ EXPECT_EQ(out.returned, 0u);
+}
+
+TEST(ContractTestingQSB, TestFilledOrders_RingBufferOverwritesOldEntries)
+{
+ ContractTestingQSB test;
+
+ // Artificially mark more orders as filled than the ring capacity
+ // to ensure oldest entries are overwritten while newer ones remain.
+ for (uint32 i = 0; i < QSB_MAX_FILLED_ORDERS + 1; ++i)
+ {
+ QSB::OrderHash hash;
+ setMemory(hash, 0);
+ // Encode i into the first two bytes to avoid collisions when
+ // QSB_MAX_FILLED_ORDERS exceeds 255.
+ hash.set(0, (uint8)(i & 0xff));
+ hash.set(1, (uint8)((i >> 8) & 0xff));
+ test.getState()->forceMarkOrderFilled(hash);
+ }
+
+ // Hash for 0 should have been overwritten (only last QSB_MAX_FILLED_ORDERS kept)
+ QSB::OrderHash hash0;
+ setMemory(hash0, 0);
+ hash0.set(1, 0);
+ QSB::IsOrderFilled_output out0 = test.isOrderFilled(hash0);
+ EXPECT_FALSE((bool)out0.filled);
+
+ // Hash for the last inserted nonce (QSB_MAX_FILLED_ORDERS) should be present
+ QSB::OrderHash hashLast;
+ setMemory(hashLast, 0);
+ hashLast.set(0, (uint8)(QSB_MAX_FILLED_ORDERS & 0xff));
+ hashLast.set(1, (uint8)((QSB_MAX_FILLED_ORDERS >> 8) & 0xff));
+ QSB::IsOrderFilled_output outLast = test.isOrderFilled(hashLast);
+ EXPECT_TRUE((bool)outLast.filled);
+}
+
+// ============================================================================
+// Initialization Tests
+// ============================================================================
+
+TEST(ContractTestingQSB, TestInitialization)
+{
+ ContractTestingQSB test;
+
+ // Check initial state
+ test.getState()->checkAdmin(ADMIN);
+ test.getState()->checkPaused(false);
+ test.getState()->checkOracleThreshold(67); // Default 67%
+ test.getState()->checkOracleCount(0);
+ test.getState()->checkBpsFee(0);
+ test.getState()->checkProtocolFee(0);
+
+ test.getState()->checkProtocolFeeRecipient(NULL_ID);
+ test.getState()->checkOracleFeeRecipient(NULL_ID);
+}
+
+// ============================================================================
+// Lock Function Tests
+// ============================================================================
+
+TEST(ContractTestingQSB, TestLock_Success)
+{
+ ContractTestingQSB test;
+
+ const uint64 amount = 1000000;
+ const uint64 relayerFee = 10000;
+ const uint32 networkOut = 1; // Solana
+ const uint32 nonce = 1;
+
+ // User should have enough balance
+ increaseEnergy(USER1, amount);
+
+ QSB::Lock_output output = test.lock(USER1, amount, relayerFee, networkOut, nonce, ContractTestingQSB::createZeroAddress(), amount);
+ EXPECT_TRUE(output.success);
+
+ // Check that orderHash is non-zero
+ bool hashNonZero = false;
+ for (uint32 i = 0; i < output.orderHash.capacity(); ++i)
+ {
+ if (output.orderHash.get(i) != 0)
+ {
+ hashNonZero = true;
+ break;
+ }
+ }
+ EXPECT_TRUE(hashNonZero);
+}
+
+TEST(ContractTestingQSB, TestLock_FailsWhenPaused)
+{
+ ContractTestingQSB test;
+
+ increaseEnergy(ADMIN, 1);
+ increaseEnergy(USER1, 1000000);
+
+ // Pause
+ test.pause(ADMIN);
+
+ // Now try to lock - should fail
+ const uint64 amount = 1000000;
+ long long balanceBefore = getBalance(USER1);
+
+ QSB::Lock_output output = test.lock(USER1, amount, 10000, 1, 2, ContractTestingQSB::createZeroAddress(), amount);
+ EXPECT_FALSE(output.success);
+
+ long long balanceAfter = getBalance(USER1);
+ EXPECT_EQ(balanceAfter, balanceBefore);
+}
+
+TEST(ContractTestingQSB, TestLock_FailsWhenRelayerFeeTooHigh)
+{
+ ContractTestingQSB test;
+
+ const uint64 amount = 1000000;
+ increaseEnergy(USER1, amount);
+
+ QSB::Lock_output output = test.lock(USER1, amount, 1000000, 1, 3, ContractTestingQSB::createZeroAddress(), amount);
+ EXPECT_FALSE(output.success);
+}
+
+TEST(ContractTestingQSB, TestLock_FailsWhenAmountIsZero)
+{
+ ContractTestingQSB test;
+
+ const uint64 amount = 0;
+ const uint64 relayerFee = 0;
+ const uint32 nonce = 40;
+
+ // No energy needed since amount is zero, but helper still expects an energyAmount argument
+ QSB::Lock_output output = test.lock(USER1, amount, relayerFee, 1, nonce, ContractTestingQSB::createZeroAddress(), 0);
+ EXPECT_FALSE(output.success);
+}
+
+TEST(ContractTestingQSB, TestLock_SucceedsWhenRelayerFeeIsAmountMinusOne)
+{
+ ContractTestingQSB test;
+
+ const uint64 amount = 1000000;
+ const uint64 relayerFee = amount - 1;
+ const uint32 nonce = 41;
+
+ increaseEnergy(USER1, amount);
+
+ QSB::Lock_output output = test.lock(USER1, amount, relayerFee, 1, nonce, ContractTestingQSB::createZeroAddress(), amount);
+ EXPECT_TRUE(output.success);
+}
+
+TEST(ContractTestingQSB, TestLock_FailsWhenInvocationRewardTooLowAndIsRefunded)
+{
+ ContractTestingQSB test;
+
+ const uint64 amount = 1000000;
+ const uint64 relayerFee = 10000;
+ const uint32 nonce = 42;
+
+ // User only sends half the required amount as invocationReward
+ increaseEnergy(USER1, amount / 2);
+ long long balanceBefore = getBalance(USER1);
+
+ QSB::Lock_output output = test.lock(USER1, amount, relayerFee, 1, nonce, ContractTestingQSB::createZeroAddress(), amount / 2);
+ EXPECT_FALSE(output.success);
+
+ long long balanceAfter = getBalance(USER1);
+ EXPECT_EQ(balanceAfter, balanceBefore);
+}
+
+TEST(ContractTestingQSB, TestLock_RingBufferOverwritesOldLockedOrders)
+{
+ ContractTestingQSB test;
+
+ const uint64 amount = 1;
+ const uint64 relayerFee = 0;
+
+ // Fill all available locked order slots with unique nonces
+ for (uint32 i = 0; i < QSB_MAX_LOCKED_ORDERS; ++i)
+ {
+ increaseEnergy(USER1, amount);
+ QSB::Lock_output out = test.lock(USER1, amount, relayerFee, 1, i, ContractTestingQSB::createZeroAddress(), amount);
+ EXPECT_TRUE(out.success);
+ }
+
+ // Next lock should still succeed, but the ring buffer will overwrite
+ // one of the older entries. The very first nonce (0) should no longer
+ // be queryable via GetLockedOrder, while the latest nonce should exist.
+ const uint32 oldestNonce = 0;
+ const uint32 newestNonce = QSB_MAX_LOCKED_ORDERS;
+
+ increaseEnergy(USER1, amount);
+ QSB::Lock_output overflowOut = test.lock(USER1, amount, relayerFee, 1, newestNonce, ContractTestingQSB::createZeroAddress(), amount);
+ EXPECT_TRUE(overflowOut.success);
+
+ QSB::GetLockedOrder_output oldest = test.getLockedOrder(oldestNonce);
+ EXPECT_FALSE((bool)oldest.exists);
+
+ QSB::GetLockedOrder_output newest = test.getLockedOrder(newestNonce);
+ EXPECT_TRUE((bool)newest.exists);
+ EXPECT_EQ(newest.order.nonce, newestNonce);
+}
+
+TEST(ContractTestingQSB, TestLock_FailsWhenNonceAlreadyUsedAndRefunds)
+{
+ ContractTestingQSB test;
+
+ const uint64 amount = 1000000;
+ const uint64 relayerFee = 10000;
+ const uint32 nonce = 43;
+
+ increaseEnergy(USER1, amount);
+ QSB::Lock_output first = test.lock(USER1, amount, relayerFee, 1, nonce, ContractTestingQSB::createZeroAddress(), amount);
+ EXPECT_TRUE(first.success);
+
+ // Second attempt with same nonce should fail and refund invocationReward
+ increaseEnergy(USER1, amount);
+ long long balanceBefore = getBalance(USER1);
+
+ QSB::Lock_output second = test.lock(USER1, amount, relayerFee, 1, nonce, ContractTestingQSB::createZeroAddress(), amount);
+ EXPECT_FALSE(second.success);
+
+ long long balanceAfter = getBalance(USER1);
+ EXPECT_EQ(balanceAfter, balanceBefore);
+}
+
+TEST(ContractTestingQSB, TestLock_FailsWhenNonceAlreadyUsed)
+{
+ ContractTestingQSB test;
+
+ const uint64 amount = 1000000;
+ const uint64 relayerFee = 10000;
+ const uint32 nonce = 4;
+
+ increaseEnergy(USER1, amount);
+
+ // First lock should succeed
+ QSB::Lock_output output = test.lock(USER1, amount, relayerFee, 1, nonce, ContractTestingQSB::createZeroAddress(), amount);
+ EXPECT_TRUE(output.success);
+
+ // Second lock with same nonce should fail
+ increaseEnergy(USER1, amount);
+ QSB::Lock_output output2 = test.lock(USER1, amount, relayerFee, 1, nonce, ContractTestingQSB::createZeroAddress(), amount);
+ EXPECT_FALSE(output2.success);
+}
+
+// ============================================================================
+// OverrideLock Function Tests
+// ============================================================================
+
+TEST(ContractTestingQSB, TestOverrideLock_Success)
+{
+ ContractTestingQSB test;
+
+ const uint64 amount = 1000000;
+ const uint64 relayerFee = 10000;
+ const uint32 nonce = 5;
+
+ // First, create a lock
+ increaseEnergy(USER1, amount);
+ test.lock(USER1, amount, relayerFee, 1, nonce, ContractTestingQSB::createZeroAddress(), amount);
+
+ // Now override it
+ Array newAddress = ContractTestingQSB::createZeroAddress();
+ newAddress.set(0, 0xFF); // Change address
+
+ QSB::OverrideLock_output overrideOutput = test.overrideLock(USER1, nonce, 5000, newAddress);
+ EXPECT_TRUE(overrideOutput.success);
+}
+
+TEST(ContractTestingQSB, TestOverrideLock_FailsWhenNotOriginalSender)
+{
+ ContractTestingQSB test;
+
+ const uint64 amount = 1000000;
+ const uint64 relayerFee = 10000;
+ const uint32 nonce = 6;
+
+ // USER1 creates a lock
+ increaseEnergy(USER1, amount);
+ test.lock(USER1, amount, relayerFee, 1, nonce, ContractTestingQSB::createZeroAddress(), amount);
+
+ // USER2 tries to override - should fail
+ QSB::OverrideLock_output overrideOutput = test.overrideLock(USER2, nonce, 5000, ContractTestingQSB::createZeroAddress());
+ EXPECT_FALSE(overrideOutput.success);
+}
+
+// ============================================================================
+// Admin Function Tests
+// ============================================================================
+
+TEST(ContractTestingQSB, TestTransferAdmin_Success)
+{
+ ContractTestingQSB test;
+
+ increaseEnergy(ADMIN, 1);
+ increaseEnergy(USER1, 1);
+ QSB::TransferAdmin_output output = test.transferAdmin(ADMIN, USER1);
+ EXPECT_TRUE(output.success);
+
+ test.getState()->checkAdmin(USER1);
+}
+
+TEST(ContractTestingQSB, TestTransferAdmin_ToNullId)
+{
+ ContractTestingQSB test;
+
+ increaseEnergy(ADMIN, 1);
+ QSB::TransferAdmin_output output = test.transferAdmin(ADMIN, NULL_ID);
+ EXPECT_TRUE(output.success);
+
+ test.getState()->checkAdmin(NULL_ID);
+}
+
+TEST(ContractTestingQSB, TestTransferAdmin_FailsWhenNotAdmin)
+{
+ ContractTestingQSB test;
+
+ // First bootstrap admin
+ increaseEnergy(USER1, 1);
+ increaseEnergy(USER2, 1);
+ // Now USER1 tries to transfer admin - should fail
+ QSB::TransferAdmin_output output = test.transferAdmin(USER1, USER2);
+ EXPECT_FALSE(output.success);
+
+ // Admin should still be ADMIN
+ test.getState()->checkAdmin(ADMIN);
+}
+
+TEST(ContractTestingQSB, TestEditOracleThreshold_Success)
+{
+ ContractTestingQSB test;
+
+ // Bootstrap admin
+ increaseEnergy(ADMIN, 1);
+
+ QSB::EditOracleThreshold_output output = test.editOracleThreshold(ADMIN, 75);
+ EXPECT_TRUE(output.success);
+ EXPECT_EQ(output.oldThreshold, 67); // Original default
+
+ test.getState()->checkOracleThreshold(75);
+}
+
+TEST(ContractTestingQSB, TestAddRole_Oracle)
+{
+ ContractTestingQSB test;
+
+ // Bootstrap admin
+ increaseEnergy(ADMIN, 1);
+
+ QSB::AddRole_output output = test.addRole(ADMIN, (uint8)QSB::Role::Oracle, ORACLE1);
+ EXPECT_TRUE(output.success);
+
+ test.getState()->checkOracleCount(1);
+}
+
+TEST(ContractTestingQSB, TestAddRole_Pauser)
+{
+ ContractTestingQSB test;
+
+ // Bootstrap admin
+ increaseEnergy(ADMIN, 1);
+ increaseEnergy(PAUSER1, 1);
+
+ QSB::AddRole_output output = test.addRole(ADMIN, (uint8)QSB::Role::Pauser, PAUSER1);
+ EXPECT_TRUE(output.success);
+}
+
+TEST(ContractTestingQSB, TestRemoveRole_Oracle)
+{
+ ContractTestingQSB test;
+
+ // Bootstrap admin and add oracle
+ increaseEnergy(ADMIN, 1);
+ test.addRole(ADMIN, (uint8)QSB::Role::Oracle, ORACLE1);
+
+ // Now remove it
+ QSB::RemoveRole_output output = test.removeRole(ADMIN, (uint8)QSB::Role::Oracle, ORACLE1);
+ EXPECT_TRUE(output.success);
+
+ test.getState()->checkOracleCount(0);
+}
+
+TEST(ContractTestingQSB, TestPause_ByAdmin)
+{
+ ContractTestingQSB test;
+
+ // Bootstrap admin
+ increaseEnergy(ADMIN, 1);
+
+ QSB::Pause_output output = test.pause(ADMIN);
+ EXPECT_TRUE(output.success);
+
+ test.getState()->checkPaused(true);
+}
+
+TEST(ContractTestingQSB, TestPause_ByPauser)
+{
+ ContractTestingQSB test;
+
+ // Bootstrap admin
+ increaseEnergy(ADMIN, 1);
+ increaseEnergy(PAUSER1, 1);
+
+ // Add pauser
+ test.addRole(ADMIN, (uint8)QSB::Role::Pauser, PAUSER1);
+
+ // Pauser can pause
+ QSB::Pause_output output = test.pause(PAUSER1);
+ EXPECT_TRUE(output.success);
+
+ test.getState()->checkPaused(true);
+}
+
+TEST(ContractTestingQSB, TestUnpause)
+{
+ ContractTestingQSB test;
+
+ // Bootstrap admin and pause
+ increaseEnergy(ADMIN, 1);
+ test.pause(ADMIN);
+
+ // Now unpause
+ QSB::Unpause_output output = test.unpause(ADMIN);
+ EXPECT_TRUE(output.success);
+
+ test.getState()->checkPaused(false);
+}
+
+TEST(ContractTestingQSB, TestEditFeeParameters)
+{
+ ContractTestingQSB test;
+
+ // Bootstrap admin
+ increaseEnergy(ADMIN, 1);
+
+ QSB::EditFeeParameters_output output = test.editFeeParameters(ADMIN, 100, 30, PROTOCOL_FEE_RECIPIENT, ORACLE_FEE_RECIPIENT);
+ EXPECT_TRUE(output.success);
+
+ test.getState()->checkBpsFee(100);
+ test.getState()->checkProtocolFee(30);
+ test.getState()->checkProtocolFeeRecipient(PROTOCOL_FEE_RECIPIENT);
+ test.getState()->checkOracleFeeRecipient(ORACLE_FEE_RECIPIENT);
+}
+
+TEST(ContractTestingQSB, TestEditFeeParameters_RejectsTooHighBpsFee)
+{
+ ContractTestingQSB test;
+
+ // Bootstrap admin
+ increaseEnergy(ADMIN, 1);
+
+ // Try to set bpsFee above the allowed maximum
+ QSB::EditFeeParameters_output output = test.editFeeParameters(ADMIN, QSB_MAX_BPS_FEE + 1, 0, NULL_ID, NULL_ID);
+ EXPECT_FALSE(output.success);
+
+ // State should remain unchanged
+ test.getState()->checkBpsFee(0);
+}
+
+TEST(ContractTestingQSB, TestEditFeeParameters_RejectsTooHighProtocolFee)
+{
+ ContractTestingQSB test;
+
+ // Bootstrap admin and set an initial valid configuration
+ increaseEnergy(ADMIN, 1);
+ test.editFeeParameters(ADMIN, 100, 10, PROTOCOL_FEE_RECIPIENT, ORACLE_FEE_RECIPIENT);
+
+ // Attempt to set protocolFee above the allowed maximum
+ QSB::EditFeeParameters_output output = test.editFeeParameters(ADMIN, 0, QSB_MAX_PROTOCOL_FEE + 1, NULL_ID, NULL_ID);
+ EXPECT_FALSE(output.success);
+
+ // State should still reflect the previous valid configuration
+ test.getState()->checkProtocolFee(10);
+}
+
+// ============================================================================
+// Unlock Function Tests
+// ============================================================================
+// Note: Full unlock testing would require valid oracle signatures
+// These tests verify the structure and basic validation logic
+
+TEST(ContractTestingQSB, TestUnlock_FailsWhenNoOracles)
+{
+ ContractTestingQSB test;
+
+ const uint64 contractFund = 2000000;
+ increaseEnergy(USER1, contractFund);
+ test.lock(USER1, contractFund, 0, 1, 1, ContractTestingQSB::createZeroAddress(), contractFund);
+
+ Array nonce32;
+ setMemory(nonce32, 0);
+ nonce32.set(0, 100);
+ QSB::Order order = ContractTestingQSB::createTestOrder(USER1, USER2, 1000000, 10000, nonce32);
+
+ Array signatures;
+ setMemory(signatures, 0);
+
+ QSB::Unlock_output output = test.unlock(USER1, order, 0, signatures);
+ EXPECT_FALSE(output.success);
+}
+
+TEST(ContractTestingQSB, TestUnlock_FailsWhenPaused)
+{
+ ContractTestingQSB test;
+
+ // Bootstrap admin, add oracle, and pause
+ increaseEnergy(ADMIN, 1);
+ test.addRole(ADMIN, (uint8)QSB::Role::Oracle, ORACLE1);
+ test.pause(ADMIN);
+
+ QSB::Order order = ContractTestingQSB::createTestOrderFromU32Nonce(USER1, USER2, 1000000, 10000, 101);
+ Array signatures;
+ setMemory(signatures, 0);
+ signatures.set(0, test.createMockSignature(ORACLE1));
+
+ QSB::Unlock_output output = test.unlock(USER1, order, 1, signatures);
+ EXPECT_FALSE(output.success); // Should fail - contract is paused
+}
+
+TEST(ContractTestingQSB, TestUnlock_FailsWhenContractBalanceTooLow)
+{
+ ContractTestingQSB test;
+
+ increaseEnergy(USER1, 1);
+ increaseEnergy(USER2, 1);
+ increaseEnergy(ORACLE1, 1);
+ // No prior locks or deposits -> contract balance should be zero
+ QSB::Order order = ContractTestingQSB::createTestOrderFromU32Nonce(USER1, USER2, 1000000, 10000, 102);
+ Array signatures;
+ setMemory(signatures, 0);
+ signatures.set(0, test.createMockSignature(ORACLE1));
+
+ QSB::Unlock_output output = test.unlock(USER1, order, 1, signatures);
+ EXPECT_FALSE(output.success);
+}
+
+TEST(ContractTestingQSB, TestUnlock_FailsWhenOrderAlreadyFilledBeforeOracleChecks)
+{
+ ContractTestingQSB test;
+
+ // Provide some contract balance via a lock, but the specific lock
+ // is intentionally unrelated to the unlock order in this model.
+ const uint64 amountLocked = 1000000;
+ increaseEnergy(USER1, amountLocked);
+ test.lock(USER1, amountLocked, 0, 1, 500, ContractTestingQSB::createZeroAddress(), amountLocked);
+
+ // Prepare an unlock order and compute its hash
+ const uint64 amount = 100000;
+ const uint64 relayerFee = 1000;
+ QSB::Order order = ContractTestingQSB::createTestOrderFromU32Nonce(USER1, USER2, amount, relayerFee, 600);
+ QSB::ComputeOrderHash_output hashOut = test.computeOrderHash(order);
+
+ // Mark this order hash as already filled via the state helper
+ test.getState()->forceMarkOrderFilled(hashOut.hash);
+
+ // Attempt unlock with no oracles and no signatures — Unlock should
+ // still fail with AlreadyFilled before it ever reaches oracle checks.
+ Array signatures;
+ setMemory(signatures, 0);
+ QSB::Unlock_output output = test.unlock(USER1, order, 0, signatures);
+ EXPECT_FALSE(output.success);
+}
+
+TEST(ContractTestingQSB, TestUnlock_DoesNotRequireMatchingLock)
+{
+ ContractTestingQSB test;
+
+ const uint64 contractFund = 2000000;
+ increaseEnergy(USER1, contractFund);
+ QSB::Lock_output lockOut = test.lock(USER1, contractFund, 0, 1, 1, ContractTestingQSB::createZeroAddress(), contractFund);
+ EXPECT_TRUE(lockOut.success);
+
+ Array nonce32;
+ setMemory(nonce32, 0);
+ nonce32.set(0, 0xAB);
+ nonce32.set(1, 0xCD);
+ QSB::Order order = ContractTestingQSB::createTestOrder(USER2, USER2, 500000, 5000, nonce32);
+
+ Array signatures;
+ setMemory(signatures, 0);
+ QSB::Unlock_output output = test.unlock(USER2, order, 0, signatures);
+ EXPECT_FALSE(output.success);
+}
+
+// ============================================================================
+// Integration Tests
+// ============================================================================
+
+TEST(ContractTestingQSB, TestFullWorkflow_LockAndOverride)
+{
+ ContractTestingQSB test;
+
+ const uint64 amount = 1000000;
+ const uint64 initialRelayerFee = 10000;
+ const uint64 newRelayerFee = 5000;
+ const uint32 nonce = 200;
+
+ // Step 1: Lock
+ increaseEnergy(USER1, amount);
+ QSB::Lock_output lockOutput = test.lock(USER1, amount, initialRelayerFee, 1, nonce, ContractTestingQSB::createZeroAddress(), amount);
+ EXPECT_TRUE(lockOutput.success);
+
+ // Step 2: Override
+ QSB::OverrideLock_output overrideOutput = test.overrideLock(USER1, nonce, newRelayerFee, ContractTestingQSB::createZeroAddress());
+ EXPECT_TRUE(overrideOutput.success);
+
+ // OrderHash should be different after override
+ bool hashesDifferent = false;
+ for (uint32 i = 0; i < lockOutput.orderHash.capacity(); ++i)
+ {
+ if (lockOutput.orderHash.get(i) != overrideOutput.orderHash.get(i))
+ {
+ hashesDifferent = true;
+ break;
+ }
+ }
+ EXPECT_TRUE(hashesDifferent);
+}
+
+TEST(ContractTestingQSB, TestAdminWorkflow_SetupAndConfigure)
+{
+ ContractTestingQSB test;
+
+ // Step 1: Bootstrap admin
+ increaseEnergy(ADMIN, 1);
+
+ // Step 2: Add oracles
+ increaseEnergy(ORACLE1, 1);
+ increaseEnergy(ORACLE2, 1);
+ increaseEnergy(ORACLE3, 1);
+ test.addRole(ADMIN, (uint8)QSB::Role::Oracle, ORACLE1);
+ test.addRole(ADMIN, (uint8)QSB::Role::Oracle, ORACLE2);
+ test.addRole(ADMIN, (uint8)QSB::Role::Oracle, ORACLE3);
+
+ test.getState()->checkOracleCount(3);
+
+ // Step 3: Set threshold
+ test.editOracleThreshold(ADMIN, 67); // 2/3 + 1
+ test.getState()->checkOracleThreshold(67);
+
+ // Step 4: Configure fees
+ test.editFeeParameters(ADMIN, 50, 20, PROTOCOL_FEE_RECIPIENT, ORACLE_FEE_RECIPIENT);
+
+ test.getState()->checkBpsFee(50);
+ test.getState()->checkProtocolFee(20);
+}
+
+// =============================================================================
+// Order Era Tests
+// =============================================================================
+
+TEST(ContractTestingQSB, TestGetConfig_ReturnsOrderEra)
+{
+ ContractTestingQSB test;
+ QSB::GetConfig_output config = test.getConfig();
+ EXPECT_EQ(config.orderEra, 0u);
+}
+
+TEST(ContractTestingQSB, TestLock_StoresCurrentEra)
+{
+ ContractTestingQSB test;
+ increaseEnergy(ADMIN, 1);
+ uint64 amount = 10000;
+ uint32 nonce = 1;
+
+ increaseEnergy(USER1, amount);
+ QSB::Lock_output lockOutput = test.lock(USER1, amount, 0, 1, nonce, ContractTestingQSB::createZeroAddress(), amount);
+ EXPECT_TRUE(lockOutput.success);
+
+ QSB::GetLockedOrder_output lockedOrder = test.getLockedOrder(nonce);
+ EXPECT_TRUE(lockedOrder.exists);
+ EXPECT_EQ(lockedOrder.order.orderEra, 0u);
+}
+
+TEST(ContractTestingQSB, TestComputeOrderHash_DiffersByEra)
+{
+ ContractTestingQSB test;
+
+ QSB::Order order0 = ContractTestingQSB::createTestOrderFromU32Nonce(USER1, USER2, 1000, 10, 1, 0);
+ QSB::Order order1 = ContractTestingQSB::createTestOrderFromU32Nonce(USER1, USER2, 1000, 10, 1, 1);
+
+ QSB::ComputeOrderHash_output hash0 = test.computeOrderHash(order0);
+ QSB::ComputeOrderHash_output hash1 = test.computeOrderHash(order1);
+
+ bool different = false;
+ for (uint32 i = 0; i < hash0.hash.capacity(); ++i)
+ {
+ if (hash0.hash.get(i) != hash1.hash.get(i))
+ {
+ different = true;
+ break;
+ }
+ }
+ EXPECT_TRUE(different);
+}
+
+TEST(ContractTestingQSB, TestFilledOrders_EraIncrementsOnWrap)
+{
+ ContractTestingQSB test;
+
+ // era should start at 0
+ EXPECT_EQ(test.getState()->orderEra, 0u);
+
+ // Force-fill 2048 orders to trigger wrap
+ for (uint32 i = 0; i < QSB_MAX_FILLED_ORDERS; ++i)
+ {
+ QSB::OrderHash hash;
+ setMemory(hash, 0);
+ hash.set(0, (uint8)(i & 0xFF));
+ hash.set(1, (uint8)((i >> 8) & 0xFF));
+ test.getState()->forceMarkOrderFilled(hash);
+ }
+
+ // After 2048 fills, era should have incremented to 1
+ EXPECT_EQ(test.getState()->orderEra, 1u);
+}
+
+TEST(ContractTestingQSB, TestOverrideLock_PreservesOriginalEra)
+{
+ ContractTestingQSB test;
+ increaseEnergy(ADMIN, 1);
+ uint64 amount = 10000;
+ uint32 nonce = 42;
+
+ increaseEnergy(USER1, amount);
+ QSB::Lock_output lockOutput = test.lock(USER1, amount, 100, 1, nonce, ContractTestingQSB::createZeroAddress(), amount);
+ EXPECT_TRUE(lockOutput.success);
+
+ QSB::GetLockedOrder_output before = test.getLockedOrder(nonce);
+ EXPECT_TRUE(before.exists);
+ EXPECT_EQ(before.order.orderEra, 0u);
+
+ // Force era to 1 by filling the ring buffer
+ for (uint32 i = 0; i < QSB_MAX_FILLED_ORDERS; ++i)
+ {
+ QSB::OrderHash hash;
+ setMemory(hash, 0);
+ hash.set(0, (uint8)(i & 0xFF));
+ hash.set(1, (uint8)((i >> 8) & 0xFF));
+ test.getState()->forceMarkOrderFilled(hash);
+ }
+ EXPECT_EQ(test.getState()->orderEra, 1u);
+
+ // OverrideLock should preserve the original era (0)
+ QSB::OverrideLock_output overrideOutput = test.overrideLock(USER1, nonce, 50, ContractTestingQSB::createZeroAddress());
+ EXPECT_TRUE(overrideOutput.success);
+
+ QSB::GetLockedOrder_output after = test.getLockedOrder(nonce);
+ EXPECT_TRUE(after.exists);
+ EXPECT_EQ(after.order.orderEra, 0u);
+}
+
+TEST(ContractTestingQSB, TestUnlock_FailsWhenEraMismatch)
+{
+ ContractTestingQSB test;
+ increaseEnergy(ADMIN, 1);
+
+ // Setup: add oracle, set threshold
+ increaseEnergy(ORACLE1, 1);
+ test.addRole(ADMIN, (uint8)QSB::Role::Oracle, ORACLE1);
+ test.editOracleThreshold(ADMIN, 1);
+
+ // Fund contract with some balance
+ uint64 amount = 10000;
+ increaseEnergy(USER1, amount);
+ test.lock(USER1, amount, 0, 1, 1, ContractTestingQSB::createZeroAddress(), amount);
+
+ // Create order with era=5 while state is at era=0
+ QSB::Order order = ContractTestingQSB::createTestOrderFromU32Nonce(USER1, USER2, amount, 10, 99, 5);
+
+ Array sigs;
+ setMemory(sigs, 0);
+ sigs.set(0, test.createMockSignature(ORACLE1));
+
+ QSB::Unlock_output unlockOutput = test.unlock(USER1, order, 1, sigs);
+ EXPECT_FALSE(unlockOutput.success);
+}
+
+TEST(ContractTestingQSB, TestUnlock_FailsWhenEraIsTooOld)
+{
+ ContractTestingQSB test;
+ increaseEnergy(ADMIN, 1);
+
+ // Setup oracle
+ increaseEnergy(ORACLE1, 1);
+ test.addRole(ADMIN, (uint8)QSB::Role::Oracle, ORACLE1);
+ test.editOracleThreshold(ADMIN, 1);
+
+ // Force era to 3 by filling ring buffer 3 times
+ for (uint32 round = 0; round < 3; ++round)
+ {
+ for (uint32 i = 0; i < QSB_MAX_FILLED_ORDERS; ++i)
+ {
+ QSB::OrderHash hash;
+ setMemory(hash, 0);
+ hash.set(0, (uint8)(i & 0xFF));
+ hash.set(1, (uint8)((i >> 8) & 0xFF));
+ hash.set(2, (uint8)(round & 0xFF));
+ test.getState()->forceMarkOrderFilled(hash);
+ }
+ }
+ EXPECT_EQ(test.getState()->orderEra, 3u);
+
+ // era=1 is rejected (current=3, only era=3 accepted — no grace period)
+ QSB::Order orderOld = ContractTestingQSB::createTestOrderFromU32Nonce(USER1, USER2, 100, 10, 99, 1);
+ // Fund contract
+ increaseEnergy(USER1, 100);
+ test.lock(USER1, 100, 0, 1, 50, ContractTestingQSB::createZeroAddress(), 100);
+
+ Array sigs;
+ setMemory(sigs, 0);
+ sigs.set(0, test.createMockSignature(ORACLE1));
+
+ QSB::Unlock_output unlockOld = test.unlock(USER1, orderOld, 1, sigs);
+ EXPECT_FALSE(unlockOld.success); // fails due to era mismatch
+}
diff --git a/test/test.vcxproj b/test/test.vcxproj
index 88f9f3fd..5d1ddb13 100644
--- a/test/test.vcxproj
+++ b/test/test.vcxproj
@@ -127,6 +127,7 @@
+
diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters
index 42a4b83f..fe8aa89c 100644
--- a/test/test.vcxproj.filters
+++ b/test/test.vcxproj.filters
@@ -49,6 +49,7 @@
+