From 17df450512b266ec293ea5d99d71259fb4cb361e Mon Sep 17 00:00:00 2001 From: Soufiane Benbah Date: Thu, 26 Mar 2026 17:46:11 +0100 Subject: [PATCH 1/3] Add QubicSolanaBridge smart contract (QSB, index 27) --- src/Qubic.vcxproj | 1 + src/Qubic.vcxproj.filters | 3 + src/contract_core/contract_def.h | 10 + src/contracts/QubicSolanaBridge.h | 1951 +++++++++++++++++++++++++++++ test/contract_qsb.cpp | 1509 ++++++++++++++++++++++ test/test.vcxproj | 1 + test/test.vcxproj.filters | 1 + 7 files changed, 3476 insertions(+) create mode 100644 src/contracts/QubicSolanaBridge.h create mode 100644 test/contract_qsb.cpp diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 2466a82d..86c3bf9c 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 102571a1..85b1d903 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -309,6 +309,9 @@ contracts + + contracts + contracts diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index b29f706f..de64cf7f 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..a4d5fa2b --- /dev/null +++ b/src/contracts/QubicSolanaBridge.h @@ -0,0 +1,1951 @@ +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 + +// Serialized order message: domain prefix (52 bytes) + order fields (188 bytes) = 240 bytes. +// Layout matches the oracle's serializeBridgeOrder format exactly. +#pragma pack(push, 1) +struct QSBOrderMessage +{ + uint32 protocolNameLen; // 0: always 11 + uint8 protocolName[11]; // 4: "QubicBridge" + uint32 protocolVersionLen; // 15: always 1 + uint8 protocolVersion[1]; // 19: "1" + uint8 contractAddress[32]; // 20: destination contract address (QSB index LE-padded) + uint32 networkIn; // 52 + uint32 networkOut; // 56 + uint8 tokenIn[32]; // 60 + uint8 tokenOut[32]; // 92 + uint8 fromAddress[32]; // 124 + uint8 toAddress[32]; // 156 + uint64 amount; // 188 + uint64 relayerFee; // 196 + uint8 nonce[32]; // 204 + uint32 orderEra; // 236 +}; +#pragma pack(pop) +static_assert(sizeof(QSBOrderMessage) == 240, "OrderMessage must be exactly 240 bytes"); +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 const uint32 QSBLogLock = 1; +static const uint32 QSBLogOverrideLock = 2; +static const uint32 QSBLogUnlock = 3; +static const uint32 QSBLogPaused = 4; +static const uint32 QSBLogUnpaused = 5; +static const uint32 QSBLogAdminTransferred = 6; +static const uint32 QSBLogThresholdUpdated = 7; +static const uint32 QSBLogRoleGranted = 8; +static const uint32 QSBLogRoleRevoked = 9; +static const uint32 QSBLogFeeParametersUpdated = 10; + +// Generic reason codes for logging +static const uint8 QSBReasonNone = 0; +static const uint8 QSBReasonPaused = 1; +static const uint8 QSBReasonInvalidAmount = 2; +static const uint8 QSBReasonInsufficientReward = 3; +static const uint8 QSBReasonNonceUsed = 4; +static const uint8 QSBReasonNoSpace = 5; +static const uint8 QSBReasonNotSender = 6; +static const uint8 QSBReasonBadRelayerFee = 7; +static const uint8 QSBReasonNoOracles = 8; +static const uint8 QSBReasonThresholdFailed = 9; +static const uint8 QSBReasonAlreadyFilled = 10; +static const uint8 QSBReasonInvalidSignature = 11; +static const uint8 QSBReasonDuplicateSigner = 12; +static const uint8 QSBReasonNotAdmin = 13; +static const uint8 QSBReasonNotAdminOrPauser = 14; +static const uint8 QSBReasonInvalidThreshold = 15; +static const uint8 QSBReasonRoleExists = 16; +static const uint8 QSBReasonRoleMissing = 17; +static const uint8 QSBReasonInvalidFeeParams = 18; +static const uint8 QSBReasonTransferFailed = 19; +static const 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[0]='Q'; msg.protocolName[1]='u'; msg.protocolName[2]='b'; + msg.protocolName[3]='i'; msg.protocolName[4]='c'; msg.protocolName[5]='B'; + msg.protocolName[6]='r'; msg.protocolName[7]='i'; msg.protocolName[8]='d'; + msg.protocolName[9]='g'; msg.protocolName[10]='e'; + msg.protocolVersionLen = 1; + msg.protocolVersion[0] = '1'; + msg.contractAddress[0] = (uint8)(CONTRACT_INDEX & 0xFF); + msg.contractAddress[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[i] = order.tokenIn.get(i); + for (i = 0; i < 32; ++i) msg.tokenOut[i] = order.tokenOut.get(i); + tmpIdBytes.setMem(order.fromAddress); + for (i = 0; i < 32; ++i) msg.fromAddress[i] = tmpIdBytes.get(i); + tmpIdBytes.setMem(order.toAddress); + for (i = 0; i < 32; ++i) msg.toAddress[i] = tmpIdBytes.get(i); + msg.amount = order.amount; + msg.relayerFee = order.relayerFee; + for (i = 0; i < 32; ++i) msg.nonce[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; + + // ----------------------------------------------------------------- + // Token transfers + // ----------------------------------------------------------------- + + locals.allTransfersOk = true; + + // 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; + } + } + + // Recipient payout + if (locals.recipientAmount > 0 && !isZero(input.order.toAddress)) + { + if (qpi.transfer(input.order.toAddress, (sint64)locals.recipientAmount) < 0) + { + locals.allTransfersOk = false; + } + } + + // If any transfer failed, do not mark the order as filled + if (!locals.allTransfersOk) + { + locals.logMsg.reasonCode = QSBReasonTransferFailed; + LOG_INFO(locals.logMsg); + return; + } + + // Mark order as filled + markOrderFilled(state, locals.hash, 0, 0, 0, locals.entry); + + 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 0cb26596..70755176 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -128,6 +128,7 @@ + diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index b428f18b..4f9aaf6b 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -49,6 +49,7 @@ + From 65892d193c556aad3db123089160e45e8e3d0d96 Mon Sep 17 00:00:00 2001 From: YazhuEth Date: Thu, 9 Apr 2026 11:23:08 +0200 Subject: [PATCH 2/3] fix: resolve qubic sc compliance violations in QSBOrderMessage - Replace #pragma pack and C-style arrays with QPI Array - Replace char/string literals with ASCII codes - Replace [i] indexing with .set(i) / .get(i) - Change static const to static constexpr - Mark order filled before transfers to prevent replay on partial failure - Reorder transfers: recipient first, then relayer and fees - QSBOrderMessage layout changes from 240 to 245 bytes (protocolName padded to 16) - Contract verifier: PASSED --- src/contracts/QubicSolanaBridge.h | 159 ++++++++++++++++-------------- 1 file changed, 83 insertions(+), 76 deletions(-) diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h index a4d5fa2b..24f59488 100644 --- a/src/contracts/QubicSolanaBridge.h +++ b/src/contracts/QubicSolanaBridge.h @@ -11,65 +11,62 @@ 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 -// Serialized order message: domain prefix (52 bytes) + order fields (188 bytes) = 240 bytes. -// Layout matches the oracle's serializeBridgeOrder format exactly. -#pragma pack(push, 1) +// 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 - uint8 protocolName[11]; // 4: "QubicBridge" - uint32 protocolVersionLen; // 15: always 1 - uint8 protocolVersion[1]; // 19: "1" - uint8 contractAddress[32]; // 20: destination contract address (QSB index LE-padded) - uint32 networkIn; // 52 - uint32 networkOut; // 56 - uint8 tokenIn[32]; // 60 - uint8 tokenOut[32]; // 92 - uint8 fromAddress[32]; // 124 - uint8 toAddress[32]; // 156 - uint64 amount; // 188 - uint64 relayerFee; // 196 - uint8 nonce[32]; // 204 - uint32 orderEra; // 236 + 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 }; -#pragma pack(pop) -static_assert(sizeof(QSBOrderMessage) == 240, "OrderMessage must be exactly 240 bytes"); 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 const uint32 QSBLogLock = 1; -static const uint32 QSBLogOverrideLock = 2; -static const uint32 QSBLogUnlock = 3; -static const uint32 QSBLogPaused = 4; -static const uint32 QSBLogUnpaused = 5; -static const uint32 QSBLogAdminTransferred = 6; -static const uint32 QSBLogThresholdUpdated = 7; -static const uint32 QSBLogRoleGranted = 8; -static const uint32 QSBLogRoleRevoked = 9; -static const uint32 QSBLogFeeParametersUpdated = 10; +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 const uint8 QSBReasonNone = 0; -static const uint8 QSBReasonPaused = 1; -static const uint8 QSBReasonInvalidAmount = 2; -static const uint8 QSBReasonInsufficientReward = 3; -static const uint8 QSBReasonNonceUsed = 4; -static const uint8 QSBReasonNoSpace = 5; -static const uint8 QSBReasonNotSender = 6; -static const uint8 QSBReasonBadRelayerFee = 7; -static const uint8 QSBReasonNoOracles = 8; -static const uint8 QSBReasonThresholdFailed = 9; -static const uint8 QSBReasonAlreadyFilled = 10; -static const uint8 QSBReasonInvalidSignature = 11; -static const uint8 QSBReasonDuplicateSigner = 12; -static const uint8 QSBReasonNotAdmin = 13; -static const uint8 QSBReasonNotAdminOrPauser = 14; -static const uint8 QSBReasonInvalidThreshold = 15; -static const uint8 QSBReasonRoleExists = 16; -static const uint8 QSBReasonRoleMissing = 17; -static const uint8 QSBReasonInvalidFeeParams = 18; -static const uint8 QSBReasonTransferFailed = 19; -static const uint8 QSBReasonEraMismatch = 20; +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 @@ -538,14 +535,21 @@ struct QSB : public ContractBase { setMemory(msg, 0); msg.protocolNameLen = 11; - msg.protocolName[0]='Q'; msg.protocolName[1]='u'; msg.protocolName[2]='b'; - msg.protocolName[3]='i'; msg.protocolName[4]='c'; msg.protocolName[5]='B'; - msg.protocolName[6]='r'; msg.protocolName[7]='i'; msg.protocolName[8]='d'; - msg.protocolName[9]='g'; msg.protocolName[10]='e'; + 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[0] = '1'; - msg.contractAddress[0] = (uint8)(CONTRACT_INDEX & 0xFF); - msg.contractAddress[1] = (uint8)((CONTRACT_INDEX >> 8) & 0xFF); + 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( @@ -557,15 +561,15 @@ struct QSB : public ContractBase initDomainPrefix(msg); msg.networkIn = order.networkIn; msg.networkOut = order.networkOut; - for (i = 0; i < 32; ++i) msg.tokenIn[i] = order.tokenIn.get(i); - for (i = 0; i < 32; ++i) msg.tokenOut[i] = order.tokenOut.get(i); + 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[i] = tmpIdBytes.get(i); + 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[i] = tmpIdBytes.get(i); + 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[i] = order.nonce.get(i); + for (i = 0; i < 32; ++i) msg.nonce.set(i, order.nonce.get(i)); msg.orderEra = order.orderEra; } @@ -1338,12 +1342,28 @@ struct QSB : public ContractBase 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) { @@ -1371,16 +1391,6 @@ struct QSB : public ContractBase } } - // Recipient payout - if (locals.recipientAmount > 0 && !isZero(input.order.toAddress)) - { - if (qpi.transfer(input.order.toAddress, (sint64)locals.recipientAmount) < 0) - { - locals.allTransfersOk = false; - } - } - - // If any transfer failed, do not mark the order as filled if (!locals.allTransfersOk) { locals.logMsg.reasonCode = QSBReasonTransferFailed; @@ -1388,9 +1398,6 @@ struct QSB : public ContractBase return; } - // Mark order as filled - markOrderFilled(state, locals.hash, 0, 0, 0, locals.entry); - output.success = true; locals.logMsg.success = 1; locals.logMsg.reasonCode = QSBReasonNone; From 09e330242d0a153f4736b2503905c540234638a9 Mon Sep 17 00:00:00 2001 From: YazhuEth Date: Thu, 9 Apr 2026 11:50:08 +0200 Subject: [PATCH 3/3] fix: remove QSurv references (removed upstream) --- src/Qubic.vcxproj | 1 - src/Qubic.vcxproj.filters | 1 - test/test.vcxproj | 1 - test/test.vcxproj.filters | 1 - 4 files changed, 4 deletions(-) diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index b6312346..40e230bd 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -49,7 +49,6 @@ - diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index 9e067f95..78bedbd7 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -309,7 +309,6 @@ contracts - contracts diff --git a/test/test.vcxproj b/test/test.vcxproj index cc019d5a..5d1ddb13 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -127,7 +127,6 @@ - diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index 3290c016..fe8aa89c 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -49,7 +49,6 @@ -