From f1a67197184bff140fc51fda90738aeb948cd554 Mon Sep 17 00:00:00 2001 From: Soufiane Benbah Date: Thu, 26 Mar 2026 17:46:11 +0100 Subject: [PATCH 01/28] Add QubicSolanaBridge smart contract (QSB, index 27) --- src/Qubic.vcxproj | 1 + src/Qubic.vcxproj.filters | 4 + src/contract_core/contract_def.h | 12 +- src/contracts/QubicSolanaBridge.h | 1951 +++++++++++++++++++++++++++++ test/contract_qsb.cpp | 1509 ++++++++++++++++++++++ test/test.vcxproj | 2 +- test/test.vcxproj.filters | 1 + 7 files changed, 3472 insertions(+), 8 deletions(-) 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 dfc28630..9164a5ff 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 5f7266d9..f2c49711 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -314,6 +314,10 @@ contracts + + + contracts + contracts diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 02f5de8b..699a82af 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -278,11 +278,11 @@ #undef CONTRACT_STATE_TYPE #undef CONTRACT_STATE2_TYPE -#define ESCROW_CONTRACT_INDEX 27 -#define CONTRACT_INDEX ESCROW_CONTRACT_INDEX -#define CONTRACT_STATE_TYPE ESCROW -#define CONTRACT_STATE2_TYPE ESCROW2 -#include "contracts/Escrow.h" +#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 @@ -397,7 +397,6 @@ constexpr struct ContractDescription {"PULSE", 204, 10000, sizeof(PULSE::StateData)}, // proposal in epoch 202, IPO in 203, construction and first use in 204 {"VOTTUN", 206, 10000, sizeof(VOTTUNBRIDGE::StateData)}, // proposal in epoch 204, IPO in 205, construction and first use in 206 {"QUSINO", 208, 10000, sizeof(QUSINO::StateData)}, // proposal in epoch 206, IPO in 207, construction and first use in 208 - {"ESCROW", 210, 10000, sizeof(ESCROW::StateData)}, // proposal in epoch 208, IPO in 209, construction and first use in 210 // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA::StateData)}, @@ -520,7 +519,6 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(PULSE); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(VOTTUNBRIDGE); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QUSINO); - REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(ESCROW); // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); 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 07091276..ddc9f9fa 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -127,7 +127,7 @@ - + diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index 327c4e25..2bc8fce6 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -49,6 +49,7 @@ + From 7fb40badf170d02f0c794632d711a0cd935b07e2 Mon Sep 17 00:00:00 2001 From: YazhuEth Date: Thu, 9 Apr 2026 11:23:08 +0200 Subject: [PATCH 02/28] 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/contract_core/contract_def.h | 15 ++- src/contracts/QubicSolanaBridge.h | 159 ++++++++++++++++-------------- test/test.vcxproj | 1 + test/test.vcxproj.filters | 1 + 4 files changed, 99 insertions(+), 77 deletions(-) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 699a82af..91e2cb0c 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -278,7 +278,17 @@ #undef CONTRACT_STATE_TYPE #undef CONTRACT_STATE2_TYPE -#define QSB_CONTRACT_INDEX 27 +#define ESCROW_CONTRACT_INDEX 27 +#define CONTRACT_INDEX ESCROW_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE ESCROW +#define CONTRACT_STATE2_TYPE ESCROW2 +#include "contracts/Escrow.h" + +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QSB_CONTRACT_INDEX 28 #define CONTRACT_INDEX QSB_CONTRACT_INDEX #define CONTRACT_STATE_TYPE QSB #define CONTRACT_STATE2_TYPE QSB2 @@ -397,6 +407,7 @@ constexpr struct ContractDescription {"PULSE", 204, 10000, sizeof(PULSE::StateData)}, // proposal in epoch 202, IPO in 203, construction and first use in 204 {"VOTTUN", 206, 10000, sizeof(VOTTUNBRIDGE::StateData)}, // proposal in epoch 204, IPO in 205, construction and first use in 206 {"QUSINO", 208, 10000, sizeof(QUSINO::StateData)}, // proposal in epoch 206, IPO in 207, construction and first use in 208 + {"ESCROW", 210, 10000, sizeof(ESCROW::StateData)}, // proposal in epoch 208, IPO in 209, construction and first use in 210 // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA::StateData)}, @@ -519,6 +530,8 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(PULSE); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(VOTTUNBRIDGE); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QUSINO); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(ESCROW); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QSB); // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); 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; diff --git a/test/test.vcxproj b/test/test.vcxproj index ddc9f9fa..d19b646d 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -127,6 +127,7 @@ + diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index 2bc8fce6..7e78ab2f 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -49,6 +49,7 @@ + From 8621ece5ef9758a50ef24ddfedae1ee56ec20365 Mon Sep 17 00:00:00 2001 From: Jean Date: Tue, 12 May 2026 15:32:21 +0200 Subject: [PATCH 03/28] Enable local testnet bootstrap support --- src/private_settings.h | 2 +- src/qubic.cpp | 33 +++++++++++++++++++-------------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/private_settings.h b/src/private_settings.h index 82ee9f8b..461956ef 100644 --- a/src/private_settings.h +++ b/src/private_settings.h @@ -1381,7 +1381,7 @@ static unsigned char customSeeds[][55 + 1] = { // You can find current peer IPs at https://app.qubic.li/network/live static const unsigned char knownPublicPeers[][4] = { {127, 0, 0, 1}, // DONT REMOVE THIS - // Add more node ips here + {172, 28, 0, 2}, // echo peer (Docker bridge, see core-bob/docker/examples/docker-compose.yml) }; /* Whitelisting has been disabled, as requesting the IP of the incoming connection freezes the node occasionally diff --git a/src/qubic.cpp b/src/qubic.cpp index 771dbedd..30705a3b 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -16,7 +16,7 @@ ////////////////// USER CONFIGURABLE OPTIONS (default is for mainnet with swap feature) \\\\\\\\\\\\\\\\ -// #define TESTNET // UNCOMMENT this line if you want to compile for testnet +#define TESTNET // UNCOMMENT this line if you want to compile for testnet // #define TESTNET_PREFILL_QUS // UNCOMMENT this line if you want to send test QUs to computors/custom address at epoch begin // this option enables using disk as RAM to reduce hardware requirement for qubic core node // it is highly recommended to enable this option if you want to run a full mainnet node on SSD @@ -7130,11 +7130,11 @@ static bool initialize() } } -#ifdef TESTNET - // if there missing universe for testnet, we should give 676 shares for it. - if (isAllBytesZero(assets, ASSETS_CAPACITY)) - { - AssetStorage::indexLists.rebuild(); +#ifdef TESTNET + // if there missing universe for testnet, we should give 676 shares for it. + if (isAllBytesZero(assets, ASSETS_CAPACITY)) + { + AssetStorage::indexLists.rebuild(); logToConsole(L"No universe provided, giving testnet shares to all contracts ..."); for (unsigned int i = 1; i < contractCount; i++) { @@ -7162,14 +7162,19 @@ static bool initialize() if (isAllBytesZero(contractStates[0], contractDescriptions[0].stateSize)) { logToConsole(L"No contract 0 state provided, giving testnet execution fee reserve (10B by default for each contract) ..."); - for (unsigned int i = 1; i < contractCount; i++) - { - setContractFeeReserve(i, 10'000'000'000); - } - } -#endif - - initializeContractErrors(); + for (unsigned int i = 1; i < contractCount; i++) + { + setContractFeeReserve(i, 10'000'000'000); + } + } + + // Testnet bootstrap creates synthetic asset/share state before the first real + // tick. Those logs are not part of the live chain and confuse downstream + // indexers, so restart logging from a clean tick boundary after bootstrap. + logger.reset(system.initialTick); +#endif + + initializeContractErrors(); initializeContracts(); if (loadMiningSeedFromFile) From afa3e71b32b03c47fb8f7298e4451506dc778e42 Mon Sep 17 00:00:00 2001 From: Jean Date: Wed, 13 May 2026 11:22:35 +0200 Subject: [PATCH 04/28] feat(contract): register QSB at index 28 with constructionEpoch=212 to skip IPO on local testnet Co-Authored-By: Claude Sonnet 4.6 --- src/contract_core/contract_def.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 91e2cb0c..b0d6788a 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -408,6 +408,7 @@ constexpr struct ContractDescription {"VOTTUN", 206, 10000, sizeof(VOTTUNBRIDGE::StateData)}, // proposal in epoch 204, IPO in 205, construction and first use in 206 {"QUSINO", 208, 10000, sizeof(QUSINO::StateData)}, // proposal in epoch 206, IPO in 207, construction and first use in 208 {"ESCROW", 210, 10000, sizeof(ESCROW::StateData)}, // proposal in epoch 208, IPO in 209, construction and first use in 210 + {"QSB", 212, 10000, sizeof(QSB::StateData)}, // local testnet: constructionEpoch <= testnet epoch to skip IPO // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA::StateData)}, From afb0b808362c6be1af788828abddfda7a3cb12fb Mon Sep 17 00:00:00 2001 From: Jean Date: Wed, 13 May 2026 12:34:42 +0200 Subject: [PATCH 05/28] feat(testnet): enable TESTNET_PREFILL_QUS and seed local test identities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Uncomment TESTNET_PREFILL_QUS so all customSeeds get 10B QU at epoch start - Add admin, oracle-1..6, pauser, relayer, user seeds to customSeeds - Fix QSB INITIALIZE() admin to real key (SINUBYSBZ… from .temp/qubic-admin.json) instead of placeholder id(100,200,300,400) Co-Authored-By: Claude Sonnet 4.6 --- src/contracts/QubicSolanaBridge.h | 3 ++- src/private_settings.h | 12 +++++++++- src/qubic.cpp | 38 +++++++++++++++---------------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h index 24f59488..055fc795 100644 --- a/src/contracts/QubicSolanaBridge.h +++ b/src/contracts/QubicSolanaBridge.h @@ -1932,7 +1932,8 @@ struct QSB : public ContractBase INITIALIZE() { // No admin set initially; first TransferAdmin call bootstraps admin. - state.mut().admin = id(100, 200, 300, 400); + // Admin = SINUBYSBZKBSVEFQDZBQWUEJWRXCXOZNKPHIXDZWRBKXDSPJEHFAMBACXHUN (.temp/qubic-admin.keys.json) + state.mut().admin = id(11994886480163374182ULL, 7222723150474050185ULL, 4187743050690849231ULL, 4967671197750064684ULL); state.mut().paused = false; state.mut().oracleThreshold = 67; // default 67% (2/3 + 1 style) diff --git a/src/private_settings.h b/src/private_settings.h index 461956ef..6466322d 100644 --- a/src/private_settings.h +++ b/src/private_settings.h @@ -1371,7 +1371,17 @@ static unsigned char broadcastedComputorSeeds[][55 + 1] = { // If you want to fund any of your custom seeds with initial spectrum, add them here. static unsigned char customSeeds[][55 + 1] = { - "qubicorelitebyfeiyuivqubicqubicqubicqubicqubicquicqubic" + "qubicorelitebyfeiyuivqubicqubicqubicqubicqubicquicqubic", // legacy funder + "eraaastggldisjhoojaekgyimrsddjxbvgaawswfvnvaygqmusnkevv", // admin (.temp/qubic-admin.keys.json) + "sgwnpzidgxbclnisgehigeculaejjxedzdkjyyfrzgzvuojrhdzywfh", // oracle-1 (.temp/oracle-1.qubic.keys.json) + "xeejtwxqrrlvacapbujaleejhbrsnnpvviknskemmgdihggpssjjkrg", // oracle-2 (.temp/oracle-2.qubic.keys.json) + "hwrmwgyjvytgemdqcewrufgumgukfsvgudaqnujykjnindlaxkjzrke", // oracle-3 (.temp/oracle-3.qubic.keys.json) + "pvdlzxjxnzbrlutlcvjfnmcmwmyyjzifczztqycnultdaekezffkpdz", // oracle-4 (.temp/oracle-4.qubic.keys.json) + "apmtsmsnrawvzwdympngnxfivnktidmfdhtltprsepmryihmeqteokh", // oracle-5 (.temp/oracle-5.qubic.keys.json) + "knxhupfxcfyvkrrdawbkotquiqrgzlijmltmxmpddtprtkmvmmvrxoc", // oracle-6 (.temp/oracle-6.qubic.keys.json) + "egvjaaxjhmdchoumtbqpfitscgjknqmvsvgyodkydmdzcfinafxopmg", // pauser (.temp/qubic-pauser.keys.json) + "fuhvrgshfjkowctygylvovlcxhzajduyltrrqjexvmtvnhlxylvdnoa", // relayer (.temp/qubic-relayer.keys.json) + "fpfccuyllfkhdadcwasfyjrcwjaoqrxizaqpiltnhbxtnoywlduhrem", // user (.temp/qubic-user.keys.json) }; // number of private ips for computor's internal services diff --git a/src/qubic.cpp b/src/qubic.cpp index 30705a3b..856d911f 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -17,7 +17,7 @@ ////////////////// USER CONFIGURABLE OPTIONS (default is for mainnet with swap feature) \\\\\\\\\\\\\\\\ #define TESTNET // UNCOMMENT this line if you want to compile for testnet -// #define TESTNET_PREFILL_QUS // UNCOMMENT this line if you want to send test QUs to computors/custom address at epoch begin +#define TESTNET_PREFILL_QUS // UNCOMMENT this line if you want to send test QUs to computors/custom address at epoch begin // this option enables using disk as RAM to reduce hardware requirement for qubic core node // it is highly recommended to enable this option if you want to run a full mainnet node on SSD // UNCOMMENT this line to enable it @@ -7130,11 +7130,11 @@ static bool initialize() } } -#ifdef TESTNET - // if there missing universe for testnet, we should give 676 shares for it. - if (isAllBytesZero(assets, ASSETS_CAPACITY)) - { - AssetStorage::indexLists.rebuild(); +#ifdef TESTNET + // if there missing universe for testnet, we should give 676 shares for it. + if (isAllBytesZero(assets, ASSETS_CAPACITY)) + { + AssetStorage::indexLists.rebuild(); logToConsole(L"No universe provided, giving testnet shares to all contracts ..."); for (unsigned int i = 1; i < contractCount; i++) { @@ -7162,19 +7162,19 @@ static bool initialize() if (isAllBytesZero(contractStates[0], contractDescriptions[0].stateSize)) { logToConsole(L"No contract 0 state provided, giving testnet execution fee reserve (10B by default for each contract) ..."); - for (unsigned int i = 1; i < contractCount; i++) - { - setContractFeeReserve(i, 10'000'000'000); - } - } - - // Testnet bootstrap creates synthetic asset/share state before the first real - // tick. Those logs are not part of the live chain and confuse downstream - // indexers, so restart logging from a clean tick boundary after bootstrap. - logger.reset(system.initialTick); -#endif - - initializeContractErrors(); + for (unsigned int i = 1; i < contractCount; i++) + { + setContractFeeReserve(i, 10'000'000'000); + } + } + + // Testnet bootstrap creates synthetic asset/share state before the first real + // tick. Those logs are not part of the live chain and confuse downstream + // indexers, so restart logging from a clean tick boundary after bootstrap. + logger.reset(system.initialTick); +#endif + + initializeContractErrors(); initializeContracts(); if (loadMiningSeedFromFile) From 605ea48d0befda6c4b4caa1950e32695c350366c Mon Sep 17 00:00:00 2001 From: Jean Date: Wed, 20 May 2026 10:28:44 +0200 Subject: [PATCH 06/28] fix(testnet): prevent epoch transitions from breaking single-node testnet - Set TESTNET_EPOCH_DURATION to 1,000,000 ticks (~5.8 days at 500ms/tick) to avoid epoch transitions during QA sessions - Set PAUSE_BEFORE_CLEAR_MEMORY=0 so epoch transitions auto-complete without requiring an interactive F10 keypress (incompatible with Docker) - Remove ONLY_LOGGING=ON cmake flag from Dockerfile Co-Authored-By: Claude Sonnet 4.6 --- docker/orchestrator/Dockerfile | 1 - src/public_settings.h | 2 +- src/qubic.cpp | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docker/orchestrator/Dockerfile b/docker/orchestrator/Dockerfile index 8461bdb9..2f794da9 100644 --- a/docker/orchestrator/Dockerfile +++ b/docker/orchestrator/Dockerfile @@ -36,7 +36,6 @@ RUN rm -rf build \ -D BUILD_BINARY:BOOL=ON \ -D CMAKE_BUILD_TYPE=Release \ -D ENABLE_AVX512=OFF \ - -D ONLY_LOGGING=ON \ && cmake --build . --target Qubic -- -j$(nproc) # Collect build outputs into a common location diff --git a/src/public_settings.h b/src/public_settings.h index 38719b91..ce50405f 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -148,7 +148,7 @@ static constexpr long long NEURON_VALUE_LIMIT = 1LL; #include "network_messages/common_def.h" #ifdef TESTNET -#define TESTNET_EPOCH_DURATION 3000ULL +#define TESTNET_EPOCH_DURATION 1000000ULL #define MAX_NUMBER_OF_TICKS_PER_EPOCH (TESTNET_EPOCH_DURATION + 3) #else #define MAX_NUMBER_OF_TICKS_PER_EPOCH (((((60ULL * 60 * 24 * 7 * 1000) / TICK_DURATION_FOR_ALLOCATION_MS) + NUMBER_OF_COMPUTORS - 1) / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS) diff --git a/src/qubic.cpp b/src/qubic.cpp index 856d911f..6eef3f90 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -335,7 +335,7 @@ static bool loadContractExecFeeFiles(CHAR16* directory = NULL, bool loadAccumula static bool saveRevenueComponents(CHAR16* directory = NULL); #if ENABLED_LOGGING -#define PAUSE_BEFORE_CLEAR_MEMORY 1 // Requiring operators to press F10 to clear memory (before switching epoch) +#define PAUSE_BEFORE_CLEAR_MEMORY 0 // Testnet: auto-transition epochs without waiting for F10 #else #define PAUSE_BEFORE_CLEAR_MEMORY 0 #endif From cec2531a5c9dfb7ac2e37b9752ff2a0af804bedc Mon Sep 17 00:00:00 2001 From: Jean Date: Thu, 21 May 2026 15:31:34 +0200 Subject: [PATCH 07/28] feat(testnet): fund jean wallet and reduce epoch duration - Add jean wallet seed (BYUYXZ...) to customSeeds for testnet genesis funding - Reduce TESTNET_EPOCH_DURATION from 1000000 to 10000 ticks Co-Authored-By: Claude Sonnet 4.6 --- src/private_settings.h | 1 + src/public_settings.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/private_settings.h b/src/private_settings.h index 6466322d..9f9c292b 100644 --- a/src/private_settings.h +++ b/src/private_settings.h @@ -1382,6 +1382,7 @@ static unsigned char customSeeds[][55 + 1] = { "egvjaaxjhmdchoumtbqpfitscgjknqmvsvgyodkydmdzcfinafxopmg", // pauser (.temp/qubic-pauser.keys.json) "fuhvrgshfjkowctygylvovlcxhzajduyltrrqjexvmtvnhlxylvdnoa", // relayer (.temp/qubic-relayer.keys.json) "fpfccuyllfkhdadcwasfyjrcwjaoqrxizaqpiltnhbxtnoywlduhrem", // user (.temp/qubic-user.keys.json) + "zvkkqymkeiblorlfufwgkqourvtoxtxaomjxheggflxuayywhywfabm", // jean wallet (BYUYXZKAENFJCCXMACJVENKVKDMAEASMMWSKKRRVHANCCNKPYDPQNROCQKHA) }; // number of private ips for computor's internal services diff --git a/src/public_settings.h b/src/public_settings.h index ce50405f..6f730b10 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -148,7 +148,7 @@ static constexpr long long NEURON_VALUE_LIMIT = 1LL; #include "network_messages/common_def.h" #ifdef TESTNET -#define TESTNET_EPOCH_DURATION 1000000ULL +#define TESTNET_EPOCH_DURATION 10000ULL #define MAX_NUMBER_OF_TICKS_PER_EPOCH (TESTNET_EPOCH_DURATION + 3) #else #define MAX_NUMBER_OF_TICKS_PER_EPOCH (((((60ULL * 60 * 24 * 7 * 1000) / TICK_DURATION_FOR_ALLOCATION_MS) + NUMBER_OF_COMPUTORS - 1) / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS) From b8b5547fc775acddd4f2c2a39428a8e145d6c43d Mon Sep 17 00:00:00 2001 From: Jean Date: Thu, 28 May 2026 13:47:00 +0200 Subject: [PATCH 08/28] fix: add contract to cmake list --- test/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ab97658b..e2c68714 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -35,6 +35,7 @@ add_executable( contract_nostromo.cpp contract_qbay.cpp contract_qbond.cpp + contract_qsb.cpp contract_qearn.cpp contract_qswap.cpp contract_qutil.cpp From 48fdfc705b8b606e36921db6b4d955329cb0b399 Mon Sep 17 00:00:00 2001 From: Jean Date: Thu, 28 May 2026 14:49:02 +0200 Subject: [PATCH 09/28] test(qsb): fix admin bootstrap and add struct size diagnostics INITIALIZE hardcodes the real deployment admin key; the test suite's ADMIN constant is different, so all admin-gated procedures silently failed. Fix the constructor to transfer admin from the deployment key to the test ADMIN immediately after INITIALIZE, using increaseEnergy to ensure the deployment key has a spectrum entry first. Also annotates CMakeLists.txt with why the alignment sanitizer must be disabled for contract_qsb.cpp (struct totals not divisible by 4), and adds a PrintStructSizes test to surface alignment regressions. Co-Authored-By: Claude Sonnet 4.6 --- test/CMakeLists.txt | 9 +++++++++ test/contract_qsb.cpp | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e2c68714..0989ce2a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -104,3 +104,12 @@ target_link_libraries( include(GoogleTest) gtest_discover_tests(qubic_core_tests) + +# QSB contract structs have sizes that are not multiples of 4 (e.g. empty input structs, +# single-bit output structs), which triggers the alignment sanitizer when the stack buffer +# writes its size metadata. Disable alignment sanitizer for this file only. +if(IS_CLANG OR IS_GCC) + set_source_files_properties(contract_qsb.cpp PROPERTIES + COMPILE_OPTIONS "-fno-sanitize=alignment" + ) +endif() diff --git a/test/contract_qsb.cpp b/test/contract_qsb.cpp index e6121d46..0ddd68de 100644 --- a/test/contract_qsb.cpp +++ b/test/contract_qsb.cpp @@ -2,6 +2,7 @@ #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); @@ -82,6 +83,11 @@ class ContractTestingQSB : protected ContractTesting INIT_CONTRACT(QSB); callSystemProcedure(QSB_CONTRACT_INDEX, INITIALIZE); + // INITIALIZE sets admin to the real deployment key; transfer to test ADMIN. + static const id DEPLOY_ADMIN(11994886480163374182ULL, 7222723150474050185ULL, 4187743050690849231ULL, 4967671197750064684ULL); + increaseEnergy(DEPLOY_ADMIN, 1); + transferAdmin(DEPLOY_ADMIN, ADMIN); + checkContractExecCleanup(); } @@ -1507,3 +1513,31 @@ TEST(ContractTestingQSB, TestUnlock_FailsWhenEraIsTooOld) QSB::Unlock_output unlockOld = test.unlock(USER1, orderOld, 1, sigs); EXPECT_FALSE(unlockOld.success); // fails due to era mismatch } + +TEST(ContractTestingQSB, PrintStructSizes) { +#define PRINT_QSB(fn) printf("%-22s in=%3zu out=%3zu loc=%3zu total=%4zu rem=%zu\n", \ + #fn, sizeof(QSB::fn##_input), sizeof(QSB::fn##_output), sizeof(QSB::fn##_locals), \ + sizeof(QSB::fn##_input)+sizeof(QSB::fn##_output)+sizeof(QSB::fn##_locals), \ + (sizeof(QSB::fn##_input)+sizeof(QSB::fn##_output)+sizeof(QSB::fn##_locals))%4) + PRINT_QSB(Lock); + PRINT_QSB(OverrideLock); + PRINT_QSB(Unlock); + PRINT_QSB(TransferAdmin); + PRINT_QSB(EditOracleThreshold); + PRINT_QSB(AddRole); + PRINT_QSB(RemoveRole); + PRINT_QSB(Pause); + PRINT_QSB(Unpause); + PRINT_QSB(EditFeeParameters); + PRINT_QSB(GetConfig); + PRINT_QSB(IsOracle); + PRINT_QSB(IsPauser); + PRINT_QSB(GetLockedOrder); + PRINT_QSB(IsOrderFilled); + PRINT_QSB(ComputeOrderHash); + PRINT_QSB(GetOracles); + PRINT_QSB(GetPausers); + PRINT_QSB(GetLockedOrders); + PRINT_QSB(GetFilledOrders); +#undef PRINT_QSB +} From 2839690c6b234aeb35e5ef2448e48ef30f1e4495 Mon Sep 17 00:00:00 2001 From: Jean Date: Thu, 28 May 2026 14:51:29 +0200 Subject: [PATCH 10/28] fix(qsb): restrict Unpause to admin only Pausers could previously unpause the bridge, allowing a compromised or malicious pauser key to cancel a security pause. Unpause now checks isAdmin instead of isAdminOrPauser, and logs QSBReasonNotAdmin on rejection. Tests: renamed TestUnpause to TestUnpause_ByAdmin; added TestUnpause_FailsForPauser covering the revoked case. Co-Authored-By: Claude Sonnet 4.6 --- src/contracts/QubicSolanaBridge.h | 4 ++-- test/contract_qsb.cpp | 28 ++++++++++++++++++++++------ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h index 055fc795..850496d7 100644 --- a/src/contracts/QubicSolanaBridge.h +++ b/src/contracts/QubicSolanaBridge.h @@ -1763,13 +1763,13 @@ struct QSB : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - if (!isAdminOrPauser(state, qpi.invocator(), 0)) + if (!isAdmin(state, qpi.invocator())) { locals.logMsg._contractIndex = SELF_INDEX; locals.logMsg._type = QSBLogUnpaused; locals.logMsg.caller = qpi.invocator(); locals.logMsg.success = 0; - locals.logMsg.reasonCode = QSBReasonNotAdminOrPauser; + locals.logMsg.reasonCode = QSBReasonNotAdmin; locals.logMsg._terminator = 0; LOG_INFO(locals.logMsg); return; diff --git a/test/contract_qsb.cpp b/test/contract_qsb.cpp index 0ddd68de..5e216b03 100644 --- a/test/contract_qsb.cpp +++ b/test/contract_qsb.cpp @@ -1110,21 +1110,37 @@ TEST(ContractTestingQSB, TestPause_ByPauser) test.getState()->checkPaused(true); } -TEST(ContractTestingQSB, TestUnpause) +TEST(ContractTestingQSB, TestUnpause_ByAdmin) { 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, TestUnpause_FailsForPauser) +{ + ContractTestingQSB test; + + increaseEnergy(ADMIN, 1); + increaseEnergy(PAUSER1, 1); + + test.addRole(ADMIN, (uint8)QSB::Role::Pauser, PAUSER1); + test.pause(PAUSER1); + test.getState()->checkPaused(true); + + // Pauser must not be able to cancel their own pause + QSB::Unpause_output output = test.unpause(PAUSER1); + EXPECT_FALSE(output.success); + + test.getState()->checkPaused(true); +} + TEST(ContractTestingQSB, TestEditFeeParameters) { ContractTestingQSB test; From b61cc6b73c6804d8187d35a12fd4006f72bca00b Mon Sep 17 00:00:00 2001 From: Jean Date: Thu, 28 May 2026 14:59:56 +0200 Subject: [PATCH 11/28] fix(qsb): reject TransferAdmin to NULL_ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setting admin to zero re-enables the bootstrap path in isAdmin() (isZero check returns true → everyone is admin). Guard against this by rejecting NULL_ID with the new QSBReasonInvalidAdmin reason code. TestTransferAdmin_ToNullId updated to assert failure and verify the admin is unchanged. Co-Authored-By: Claude Sonnet 4.6 --- src/contracts/QubicSolanaBridge.h | 8 ++++++++ test/contract_qsb.cpp | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h index 850496d7..74b52171 100644 --- a/src/contracts/QubicSolanaBridge.h +++ b/src/contracts/QubicSolanaBridge.h @@ -67,6 +67,7 @@ static constexpr uint8 QSBReasonRoleMissing = 17; static constexpr uint8 QSBReasonInvalidFeeParams = 18; static constexpr uint8 QSBReasonTransferFailed = 19; static constexpr uint8 QSBReasonEraMismatch = 20; +static constexpr uint8 QSBReasonInvalidAdmin = 21; // 21 reserved for future use struct QSB2 @@ -1438,6 +1439,13 @@ struct QSB : public ContractBase return; } + if (isZero(input.newAdmin)) + { + locals.logMsg.reasonCode = QSBReasonInvalidAdmin; + LOG_INFO(locals.logMsg); + return; + } + state.mut().admin = input.newAdmin; output.success = true; locals.logMsg.success = 1; diff --git a/test/contract_qsb.cpp b/test/contract_qsb.cpp index 5e216b03..e8dcce41 100644 --- a/test/contract_qsb.cpp +++ b/test/contract_qsb.cpp @@ -1005,9 +1005,10 @@ TEST(ContractTestingQSB, TestTransferAdmin_ToNullId) increaseEnergy(ADMIN, 1); QSB::TransferAdmin_output output = test.transferAdmin(ADMIN, NULL_ID); - EXPECT_TRUE(output.success); + EXPECT_FALSE(output.success); - test.getState()->checkAdmin(NULL_ID); + // Admin must not be reset to zero (which would open bootstrap for everyone) + test.getState()->checkAdmin(ADMIN); } TEST(ContractTestingQSB, TestTransferAdmin_FailsWhenNotAdmin) From 95bace96448c8fdb27c452ddc2a5657d63ba34e8 Mon Sep 17 00:00:00 2001 From: Jean Date: Thu, 28 May 2026 15:07:25 +0200 Subject: [PATCH 12/28] fix(qsb): surface silent failures in AddRole and OverrideLock AddRole had two silent failure paths: when the oracle/pauser array is full (for-loop exhausted without finding an inactive slot) and when input.role is neither Oracle nor Pauser. Both now log with the appropriate reason code and return instead of falling through silently. OverrideLock used QSBReasonNonceUsed when no order matched the nonce, which implies the nonce was already consumed by a completed fill. Replaced with QSBReasonOrderNotFound so callers can distinguish the two cases. New reason codes: QSBReasonInvalidRole (22), QSBReasonOrderNotFound (23). Tests added: TestAddRole_InvalidRole, TestAddRole_OracleFull, TestAddRole_PauserFull, TestOverrideLock_OrderNotFound. Co-Authored-By: Claude Sonnet 4.6 --- src/contracts/QubicSolanaBridge.h | 37 +++++++++++++++- test/contract_qsb.cpp | 71 ++++++++++++++++++++++++++++--- 2 files changed, 101 insertions(+), 7 deletions(-) diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h index 74b52171..9d8578a8 100644 --- a/src/contracts/QubicSolanaBridge.h +++ b/src/contracts/QubicSolanaBridge.h @@ -68,6 +68,8 @@ static constexpr uint8 QSBReasonInvalidFeeParams = 18; static constexpr uint8 QSBReasonTransferFailed = 19; static constexpr uint8 QSBReasonEraMismatch = 20; static constexpr uint8 QSBReasonInvalidAdmin = 21; +static constexpr uint8 QSBReasonInvalidRole = 22; +static constexpr uint8 QSBReasonOrderNotFound = 23; // 21 reserved for future use struct QSB2 @@ -885,7 +887,7 @@ struct QSB : public ContractBase locals.idx = findLockedOrderIndexByNonce(state, input.nonce, 0); if (locals.idx == NULL_INDEX) { - locals.logMsg.reasonCode = QSBReasonNonceUsed; + locals.logMsg.reasonCode = QSBReasonOrderNotFound; LOG_INFO(locals.logMsg); return; } @@ -1574,6 +1576,16 @@ struct QSB : public ContractBase return; } } + 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 = QSBReasonNoSpace; + locals.logMsg._terminator = 0; + LOG_INFO(locals.logMsg); + return; } else if (input.role == (uint8)Role::Pauser) { @@ -1614,6 +1626,29 @@ struct QSB : public ContractBase return; } } + 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 = QSBReasonNoSpace; + locals.logMsg._terminator = 0; + LOG_INFO(locals.logMsg); + return; + } + else + { + 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 = QSBReasonInvalidRole; + locals.logMsg._terminator = 0; + LOG_INFO(locals.logMsg); + return; } } diff --git a/test/contract_qsb.cpp b/test/contract_qsb.cpp index e8dcce41..f99df609 100644 --- a/test/contract_qsb.cpp +++ b/test/contract_qsb.cpp @@ -969,20 +969,31 @@ TEST(ContractTestingQSB, TestOverrideLock_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); } +TEST(ContractTestingQSB, TestOverrideLock_OrderNotFound) +{ + ContractTestingQSB test; + + increaseEnergy(USER1, 1); + + // Nonce 999 was never locked — should fail with OrderNotFound, not NonceUsed + QSB::OverrideLock_output output = test.overrideLock(USER1, 999, 0, ContractTestingQSB::createZeroAddress()); + EXPECT_FALSE(output.success); +} + // ============================================================================ // Admin Function Tests // ============================================================================ @@ -1065,18 +1076,66 @@ TEST(ContractTestingQSB, TestAddRole_Pauser) EXPECT_TRUE(output.success); } +TEST(ContractTestingQSB, TestAddRole_InvalidRole) +{ + ContractTestingQSB test; + increaseEnergy(ADMIN, 1); + + QSB::AddRole_output output = test.addRole(ADMIN, 99, USER1); + EXPECT_FALSE(output.success); + + test.getState()->checkOracleCount(0); +} + +TEST(ContractTestingQSB, TestAddRole_OracleFull) +{ + ContractTestingQSB test; + increaseEnergy(ADMIN, 1); + + for (uint32_t i = 0; i < QSB_MAX_ORACLES; ++i) + { + id oracle(i + 1, 0, 0, 0); + QSB::AddRole_output out = test.addRole(ADMIN, (uint8)QSB::Role::Oracle, oracle); + EXPECT_TRUE(out.success); + } + test.getState()->checkOracleCount(QSB_MAX_ORACLES); + + id extra(QSB_MAX_ORACLES + 1, 0, 0, 0); + QSB::AddRole_output output = test.addRole(ADMIN, (uint8)QSB::Role::Oracle, extra); + EXPECT_FALSE(output.success); + + test.getState()->checkOracleCount(QSB_MAX_ORACLES); +} + +TEST(ContractTestingQSB, TestAddRole_PauserFull) +{ + ContractTestingQSB test; + increaseEnergy(ADMIN, 1); + + for (uint32_t i = 0; i < QSB_MAX_PAUSERS; ++i) + { + id pauser(i + 1, 0, 0, 0); + QSB::AddRole_output out = test.addRole(ADMIN, (uint8)QSB::Role::Pauser, pauser); + EXPECT_TRUE(out.success); + } + + id extra(QSB_MAX_PAUSERS + 1, 0, 0, 0); + QSB::AddRole_output output = test.addRole(ADMIN, (uint8)QSB::Role::Pauser, extra); + EXPECT_FALSE(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); } From 551b4446f0c0f7369ea2362495ce4dd506fbcab9 Mon Sep 17 00:00:00 2001 From: Jean Date: Thu, 28 May 2026 16:29:09 +0200 Subject: [PATCH 13/28] feat(qsb): double-buffer replay protection with era grace window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reduce QSB_MAX_FILLED_ORDERS from 2048 to 256 (power-of-2 per QPI::Array constraint; 5x the ~50 max concurrent orders) - Add filledOrdersPrev buffer: on ring wrap, copy filledOrders → filledOrdersPrev, zero filledOrders, then increment orderEra - isOrderFilled checks both buffers, blocking replays across the era boundary - Unlock now accepts orderEra == currentEra or currentEra - 1 (grace window for in-flight orders signed just before a wrap) - Add makeOracleKey/createOrderSignature test helpers (real FourQ signatures) - Add TestUnlock_PreviousEraAccepted, TestUnlock_TwoErasAgoRejected, TestUnlock_NoReplayAfterEraTransition - Update TestFilledOrders_RingBufferOverwritesOldEntries for double-buffer semantics Co-Authored-By: Claude Sonnet 4.6 --- src/contracts/QubicSolanaBridge.h | 35 ++++- test/contract_qsb.cpp | 210 ++++++++++++++++++++++++++++-- 2 files changed, 228 insertions(+), 17 deletions(-) diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h index 9d8578a8..43bd21e2 100644 --- a/src/contracts/QubicSolanaBridge.h +++ b/src/contracts/QubicSolanaBridge.h @@ -6,7 +6,7 @@ using namespace QPI; 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_FILLED_ORDERS = 256; // QPI::Array requires power-of-2; 256 = 5x the ~50 max concurrent orders 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 @@ -508,6 +508,7 @@ struct QSB : public ContractBase Array oracles; Array pausers; Array filledOrders; + Array filledOrdersPrev; Array lockedOrders; uint32 lastLockedOrdersNextOverwriteIdx; uint32 lastFilledOrdersNextOverwriteIdx; @@ -668,11 +669,14 @@ struct QSB : public ContractBase state.mut().lastFilledOrdersNextOverwriteIdx = j; if (j == 0) { + // On ring buffer wrap: preserve current buffer as prev, clear current, advance era. + state.mut().filledOrdersPrev = state.get().filledOrders; + setMemory(state.mut().filledOrders, 0); state.mut().orderEra = state.get().orderEra + 1; } } - // Check whether an orderHash has already been filled + // Check whether an orderHash has already been filled (checks current and previous era buffers) 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) @@ -693,6 +697,24 @@ struct QSB : public ContractBase if (same) return true; } + for (i = 0; i < state.get().filledOrdersPrev.capacity(); ++i) + { + entry = state.get().filledOrdersPrev.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; } @@ -1214,9 +1236,11 @@ struct QSB : public ContractBase 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) + // Era validation: accept current era or the immediately previous era. + // Accepting era N-1 provides a grace window for in-flight orders signed just before + // a ring-buffer wrap, while isOrderFilled checks both buffers to prevent replays. + if (input.order.orderEra != state.get().orderEra && + !(state.get().orderEra > 0 && input.order.orderEra == state.get().orderEra - 1)) { locals.logMsg.reasonCode = QSBReasonEraMismatch; LOG_INFO(locals.logMsg); @@ -1989,6 +2013,7 @@ struct QSB : public ContractBase setMemory(state.mut().oracles, 0); setMemory(state.mut().pausers, 0); setMemory(state.mut().filledOrders, 0); + setMemory(state.mut().filledOrdersPrev, 0); setMemory(state.mut().lockedOrders, 0); // Default fee configuration: no fees(it will be decided later) diff --git a/test/contract_qsb.cpp b/test/contract_qsb.cpp index f99df609..75eb73a8 100644 --- a/test/contract_qsb.cpp +++ b/test/contract_qsb.cpp @@ -156,6 +156,67 @@ class ContractTestingQSB : protected ContractTesting return sig; } + // Derive a deterministic key pair from a u64 seed. + struct OracleKey { + id subseed; + id publicKey; + }; + static OracleKey makeOracleKey(uint64 seed) + { + OracleKey k; + k.subseed = id(seed, seed ^ 0xDEADBEEFULL, seed ^ 0xCAFEBABEULL, seed ^ 0xFEEDFACEULL); + id privateKey; + getPrivateKey(k.subseed.m256i_u8, privateKey.m256i_u8); + getPublicKey(privateKey.m256i_u8, k.publicKey.m256i_u8); + return k; + } + + // Create a cryptographically valid SignatureData for the given order. + // The signature is over the same K12(QSBOrderMessage) digest the contract verifies. + QSB::SignatureData createOrderSignature(const OracleKey& key, const QSB::Order& order) const + { + // Build order message the same way the contract does + QSBOrderMessage msg; + QSB::OrderHash tmpHash; + setMemory(msg, 0); + msg.protocolNameLen = 11; + msg.protocolName.set(0, 81); msg.protocolName.set(1, 117); msg.protocolName.set(2, 98); + msg.protocolName.set(3, 105); msg.protocolName.set(4, 99); msg.protocolName.set(5, 66); + msg.protocolName.set(6, 114); msg.protocolName.set(7, 105); msg.protocolName.set(8, 100); + msg.protocolName.set(9, 103); msg.protocolName.set(10, 101); + msg.protocolVersionLen = 1; + msg.protocolVersion.set(0, 49); + msg.contractAddress.set(0, (uint8)(QSB_CONTRACT_INDEX & 0xFF)); + msg.contractAddress.set(1, (uint8)((QSB_CONTRACT_INDEX >> 8) & 0xFF)); + msg.networkIn = order.networkIn; + msg.networkOut = order.networkOut; + for (uint32 i = 0; i < 32; ++i) msg.tokenIn.set(i, order.tokenIn.get(i)); + for (uint32 i = 0; i < 32; ++i) msg.tokenOut.set(i, order.tokenOut.get(i)); + tmpHash.setMem(order.fromAddress); + for (uint32 i = 0; i < 32; ++i) msg.fromAddress.set(i, tmpHash.get(i)); + tmpHash.setMem(order.toAddress); + for (uint32 i = 0; i < 32; ++i) msg.toAddress.set(i, tmpHash.get(i)); + msg.amount = order.amount; + msg.relayerFee = order.relayerFee; + for (uint32 i = 0; i < 32; ++i) msg.nonce.set(i, order.nonce.get(i)); + msg.orderEra = order.orderEra; + + m256i digest; + KangarooTwelve(&msg, sizeof(msg), &digest, sizeof(digest)); + + // getPrivateKey/sign require non-const pointers — copy to local buffers + id subseedCopy = key.subseed; + id pubKeyCopy = key.publicKey; + id privateKey; + getPrivateKey(subseedCopy.m256i_u8, privateKey.m256i_u8); + + QSB::SignatureData sig; + sig.signer = key.publicKey; + sign(subseedCopy.m256i_u8, pubKeyCopy.m256i_u8, digest.m256i_u8, + reinterpret_cast(&sig.signature)); + return sig; + } + // Helper to create a zero-initialized address array static Array createZeroAddress() { @@ -707,31 +768,54 @@ 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) + // Fill exactly one full buffer: entries 0..QSB_MAX_FILLED_ORDERS-1 + for (uint32 i = 0; i < QSB_MAX_FILLED_ORDERS; ++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) + // One more entry triggers era transition: hash 0 moves to filledOrdersPrev + { + QSB::OrderHash hash; + setMemory(hash, 0); + hash.set(0, (uint8)(QSB_MAX_FILLED_ORDERS & 0xff)); + hash.set(1, (uint8)((QSB_MAX_FILLED_ORDERS >> 8) & 0xff)); + test.getState()->forceMarkOrderFilled(hash); + } + EXPECT_EQ(test.getState()->orderEra, 1u); + + // Hash 0 is still found — it lives in filledOrdersPrev (grace window) QSB::OrderHash hash0; setMemory(hash0, 0); - hash0.set(1, 0); QSB::IsOrderFilled_output out0 = test.isOrderFilled(hash0); - EXPECT_FALSE((bool)out0.filled); + EXPECT_TRUE((bool)out0.filled); + + // Fill a second full buffer to push hash 0 out of filledOrdersPrev too + for (uint32 i = QSB_MAX_FILLED_ORDERS + 1; i < QSB_MAX_FILLED_ORDERS * 2; ++i) + { + QSB::OrderHash hash; + setMemory(hash, 0); + hash.set(0, (uint8)(i & 0xff)); + hash.set(1, (uint8)((i >> 8) & 0xff)); + hash.set(2, 1); // round tag to avoid hash collisions + test.getState()->forceMarkOrderFilled(hash); + } + EXPECT_EQ(test.getState()->orderEra, 2u); + + // Now hash 0 is gone from both buffers + QSB::IsOrderFilled_output out0after = test.isOrderFilled(hash0); + EXPECT_FALSE((bool)out0after.filled); - // Hash for the last inserted nonce (QSB_MAX_FILLED_ORDERS) should be present + // The last inserted hash (from round 2) is still 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)); + hashLast.set(0, (uint8)((QSB_MAX_FILLED_ORDERS * 2 - 1) & 0xff)); + hashLast.set(1, (uint8)(((QSB_MAX_FILLED_ORDERS * 2 - 1) >> 8) & 0xff)); + hashLast.set(2, 1); QSB::IsOrderFilled_output outLast = test.isOrderFilled(hashLast); EXPECT_TRUE((bool)outLast.filled); } @@ -1576,7 +1660,7 @@ TEST(ContractTestingQSB, TestUnlock_FailsWhenEraIsTooOld) } EXPECT_EQ(test.getState()->orderEra, 3u); - // era=1 is rejected (current=3, only era=3 accepted — no grace period) + // era=1 is rejected (current=3, grace window only covers era=2) QSB::Order orderOld = ContractTestingQSB::createTestOrderFromU32Nonce(USER1, USER2, 100, 10, 99, 1); // Fund contract increaseEnergy(USER1, 100); @@ -1590,6 +1674,108 @@ TEST(ContractTestingQSB, TestUnlock_FailsWhenEraIsTooOld) EXPECT_FALSE(unlockOld.success); // fails due to era mismatch } +// Helper: fill the ring buffer once to advance era by 1 +static void advanceEra(ContractTestingQSB& test, uint32 era) +{ + for (uint32 round = 0; round < era; ++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); + } + } +} + +// Unlock with era N-1 succeeds immediately after a ring-buffer wrap (grace window). +TEST(ContractTestingQSB, TestUnlock_PreviousEraAccepted) +{ + ContractTestingQSB test; + increaseEnergy(ADMIN, 1); + auto oracle = ContractTestingQSB::makeOracleKey(1001); + increaseEnergy(oracle.publicKey, 1); + test.addRole(ADMIN, (uint8)QSB::Role::Oracle, oracle.publicKey); + test.editOracleThreshold(ADMIN, 1); + + uint64 amount = 10000; + increaseEnergy(USER1, amount); + test.lock(USER1, amount, 0, 1, 1, ContractTestingQSB::createZeroAddress(), amount); + + // Advance to era 1 + advanceEra(test, 1); + EXPECT_EQ(test.getState()->orderEra, 1u); + + // Order signed with era=0 (previous era) should still succeed + QSB::Order order = ContractTestingQSB::createTestOrderFromU32Nonce(USER1, USER2, amount, 10, 42, 0); + Array sigs; + setMemory(sigs, 0); + sigs.set(0, test.createOrderSignature(oracle, order)); + QSB::Unlock_output result = test.unlock(USER1, order, 1, sigs); + EXPECT_TRUE(result.success); +} + +// Unlock with era N-2 is rejected even with the grace window. +TEST(ContractTestingQSB, TestUnlock_TwoErasAgoRejected) +{ + ContractTestingQSB test; + increaseEnergy(ADMIN, 1); + auto oracle = ContractTestingQSB::makeOracleKey(1002); + increaseEnergy(oracle.publicKey, 1); + test.addRole(ADMIN, (uint8)QSB::Role::Oracle, oracle.publicKey); + test.editOracleThreshold(ADMIN, 1); + + uint64 amount = 10000; + increaseEnergy(USER1, amount); + test.lock(USER1, amount, 0, 1, 1, ContractTestingQSB::createZeroAddress(), amount); + + // Advance to era 2 + advanceEra(test, 2); + EXPECT_EQ(test.getState()->orderEra, 2u); + + // Order signed with era=0 (two eras ago) is rejected (era check fails before sig check) + QSB::Order order = ContractTestingQSB::createTestOrderFromU32Nonce(USER1, USER2, amount, 10, 42, 0); + Array sigs; + setMemory(sigs, 0); + sigs.set(0, test.createOrderSignature(oracle, order)); + QSB::Unlock_output result = test.unlock(USER1, order, 1, sigs); + EXPECT_FALSE(result.success); +} + +// An order filled in era N-1 cannot be replayed in era N using the grace window. +TEST(ContractTestingQSB, TestUnlock_NoReplayAfterEraTransition) +{ + ContractTestingQSB test; + increaseEnergy(ADMIN, 1); + auto oracle = ContractTestingQSB::makeOracleKey(1003); + increaseEnergy(oracle.publicKey, 1); + test.addRole(ADMIN, (uint8)QSB::Role::Oracle, oracle.publicKey); + test.editOracleThreshold(ADMIN, 1); + + uint64 amount = 20000; + increaseEnergy(USER1, amount * 2); + test.lock(USER1, amount * 2, 0, 1, 1, ContractTestingQSB::createZeroAddress(), amount * 2); + + // Fill the order in era 0 + QSB::Order order = ContractTestingQSB::createTestOrderFromU32Nonce(USER1, USER2, amount, 10, 77, 0); + Array sigs; + setMemory(sigs, 0); + sigs.set(0, test.createOrderSignature(oracle, order)); + QSB::Unlock_output first = test.unlock(USER1, order, 1, sigs); + EXPECT_TRUE(first.success); + + // Advance to era 1 + advanceEra(test, 1); + EXPECT_EQ(test.getState()->orderEra, 1u); + + // Replay attempt with same order (era=0 accepted by grace window) must fail — isOrderFilled blocks it + QSB::Unlock_output replay = test.unlock(USER1, order, 1, sigs); + EXPECT_FALSE(replay.success); +} + TEST(ContractTestingQSB, PrintStructSizes) { #define PRINT_QSB(fn) printf("%-22s in=%3zu out=%3zu loc=%3zu total=%4zu rem=%zu\n", \ #fn, sizeof(QSB::fn##_input), sizeof(QSB::fn##_output), sizeof(QSB::fn##_locals), \ From 8fa1db2175017c22f4a4434bba5bfa01ec4426ab Mon Sep 17 00:00:00 2001 From: Jean Date: Thu, 28 May 2026 16:34:28 +0200 Subject: [PATCH 14/28] chore(qsb): remove ineffective per-file alignment sanitizer suppression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The set_source_files_properties block was not reliable — the alignment sanitizer fires in inline StackBuffer functions that may be linked from another TU compiled without the flag. The real fix is USE_SANITIZER=OFF at configure time. Co-Authored-By: Claude Sonnet 4.6 --- test/CMakeLists.txt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0989ce2a..e2c68714 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -104,12 +104,3 @@ target_link_libraries( include(GoogleTest) gtest_discover_tests(qubic_core_tests) - -# QSB contract structs have sizes that are not multiples of 4 (e.g. empty input structs, -# single-bit output structs), which triggers the alignment sanitizer when the stack buffer -# writes its size metadata. Disable alignment sanitizer for this file only. -if(IS_CLANG OR IS_GCC) - set_source_files_properties(contract_qsb.cpp PROPERTIES - COMPILE_OPTIONS "-fno-sanitize=alignment" - ) -endif() From fdf7c2a38b8a275133d70d306e6528772e311be3 Mon Sep 17 00:00:00 2001 From: Jean Date: Mon, 1 Jun 2026 10:25:35 +0200 Subject: [PATCH 15/28] chore(qsb): remove dead clearLockedOrderEntry and fix test helpers Remove the clearLockedOrderEntry function which was defined but never called. Replace its use in the new fillLockedOrderSlot test helper with value-initialization (= {}). Also add build-test/ to .gitignore. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 + src/contracts/QubicSolanaBridge.h | 32 +++++--------------- test/contract_qsb.cpp | 50 +++++++++++++++++++------------ 3 files changed, 40 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index 99c8682d..c62d6c72 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ x64/ .clang-format tmp build/ +build-test/ # Build directories and temporary files out/build/ diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h index 43bd21e2..ea471429 100644 --- a/src/contracts/QubicSolanaBridge.h +++ b/src/contracts/QubicSolanaBridge.h @@ -510,8 +510,8 @@ struct QSB : public ContractBase Array filledOrders; Array filledOrdersPrev; Array lockedOrders; - uint32 lastLockedOrdersNextOverwriteIdx; uint32 lastFilledOrdersNextOverwriteIdx; + uint32 lastLockedOrdersNextOverwriteIdx; uint32 oracleCount; uint32 pauserCount; uint32 bpsFee; // fee taken in BPS (base 10000) from netAmount @@ -621,21 +621,6 @@ struct QSB : public ContractBase 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) { @@ -729,6 +714,7 @@ struct QSB : public ContractBase return NULL_INDEX; } + public: // --------------------------------------------------------------------- // Core user procedures @@ -737,7 +723,6 @@ struct QSB : public ContractBase struct Lock_locals { id digest; - LockedOrderEntry existing; Order tmpOrder; LockedOrderEntry entry; QSBOrderMessage msgBuffer; @@ -840,7 +825,8 @@ struct QSB : public ContractBase locals.logMsg.orderHash = output.orderHash; locals.logMsg.orderEra = state.get().orderEra; - // Persist locked order so that overrideLock or off-chain tooling can reference it. + // Persist locked order in ring buffer. Oldest slot is overwritten when the buffer is full; + // by the time the ring wraps (1024 orders), off-chain tooling has indexed earlier entries. locals.entry.active = true; locals.entry.sender = qpi.invocator(); locals.entry.networkOut = input.networkOut; @@ -852,8 +838,6 @@ struct QSB : public ContractBase 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; @@ -2003,10 +1987,10 @@ struct QSB : public ContractBase state.mut().admin = id(11994886480163374182ULL, 7222723150474050185ULL, 4187743050690849231ULL, 4967671197750064684ULL); 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().oracleThreshold = 67; // default 67% (2/3 + 1 style) + state.mut().lastFilledOrdersNextOverwriteIdx = 0; + state.mut().lastLockedOrdersNextOverwriteIdx = 0; + state.mut().oracleCount = 0; state.mut().pauserCount = 0; // Clear role mappings and filled order table diff --git a/test/contract_qsb.cpp b/test/contract_qsb.cpp index 75eb73a8..5d7ffd5a 100644 --- a/test/contract_qsb.cpp +++ b/test/contract_qsb.cpp @@ -71,6 +71,22 @@ class StateCheckerQSB : public QSB, public QSB::StateData bool same = false; markOrderFilled(asMutState(), hash, 0, 0, same, entry); } + + // Directly write an active locked order entry into a slot (bypasses contract call overhead). + void fillLockedOrderSlot(uint32 slot, uint32 nonce) + { + LockedOrderEntry entry = {}; + entry.active = true; + entry.nonce = nonce; + entry.sender = id((uint64)(slot + 10000), 0ULL, 0ULL, 0ULL); + entry.amount = 1; + asMutState().mut().lockedOrders.set(slot, entry); + } + + void setLastLockedOrdersNextIdx(uint32 idx) + { + asMutState().mut().lastLockedOrdersNextOverwriteIdx = idx; + } }; class ContractTestingQSB : protected ContractTesting @@ -950,37 +966,33 @@ TEST(ContractTestingQSB, TestLock_FailsWhenInvocationRewardTooLowAndIsRefunded) EXPECT_EQ(balanceAfter, balanceBefore); } -TEST(ContractTestingQSB, TestLock_RingBufferOverwritesOldLockedOrders) +TEST(ContractTestingQSB, TestLock_RingBufferOverwritesOldestSlot) { ContractTestingQSB test; - const uint64 amount = 1; - const uint64 relayerFee = 0; - // Fill all available locked order slots with unique nonces + // Fill all QSB_MAX_LOCKED_ORDERS slots sequentially. 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); + QSB::Lock_output out = test.lock(USER1, amount, 0, 1, i, ContractTestingQSB::createZeroAddress(), amount); + ASSERT_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; + // Verify slot 0 holds nonce=0 (the first lock). + QSB::GetLockedOrder_output first = test.getLockedOrder(0); + ASSERT_TRUE((bool)first.exists); + EXPECT_EQ(first.order.nonce, 0u); + // One more lock (nonce=QSB_MAX_LOCKED_ORDERS) must overwrite slot 0. 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::Lock_output overflow = test.lock(USER1, amount, 0, 1, QSB_MAX_LOCKED_ORDERS, ContractTestingQSB::createZeroAddress(), amount); + EXPECT_TRUE(overflow.success); - QSB::GetLockedOrder_output newest = test.getLockedOrder(newestNonce); - EXPECT_TRUE((bool)newest.exists); - EXPECT_EQ(newest.order.nonce, newestNonce); + // Slot 0 now holds the newest nonce. + QSB::GetLockedOrder_output overwritten = test.getLockedOrder(0); + ASSERT_TRUE((bool)overwritten.exists); + EXPECT_EQ(overwritten.order.nonce, QSB_MAX_LOCKED_ORDERS); } TEST(ContractTestingQSB, TestLock_FailsWhenNonceAlreadyUsedAndRefunds) From 65c7a1d626420d31147c9e00005646cd8456e36c Mon Sep 17 00:00:00 2001 From: Jean Date: Wed, 3 Jun 2026 10:49:45 +0200 Subject: [PATCH 16/28] feat(qsb): return locked/filled orders most-recent-first GetLockedOrders and GetFilledOrders previously iterated the ring buffers from index 0 forward (oldest-first). Switch to iterating backwards from (nextOverwriteIdx - 1), so callers receive the most recently inserted entries at offset 0. Both capacities are powers of 2, enabling cheap wrap-around with bitwise AND. Add TestGetLockedOrders_MostRecentFirst and TestGetFilledOrders_MostRecentFirst to verify the ordering guarantee. Co-Authored-By: Claude Sonnet 4.6 --- src/contracts/QubicSolanaBridge.h | 14 +++++++--- test/contract_qsb.cpp | 43 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h index ea471429..df6b5f33 100644 --- a/src/contracts/QubicSolanaBridge.h +++ b/src/contracts/QubicSolanaBridge.h @@ -1064,6 +1064,7 @@ struct QSB : public ContractBase struct GetLockedOrders_locals { uint32 i; + uint32 slot; uint32 totalActive; uint32 collected; uint32 effectiveLimit; @@ -1079,9 +1080,11 @@ struct QSB : public ContractBase 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) + // Iterate most-recent-first: start one slot before the next write position + for (locals.i = 0; locals.i < QSB_MAX_LOCKED_ORDERS; ++locals.i) { - locals.entry = state.get().lockedOrders.get(locals.i); + locals.slot = (state.get().lastLockedOrdersNextOverwriteIdx + QSB_MAX_LOCKED_ORDERS - 1 - locals.i) & (QSB_MAX_LOCKED_ORDERS - 1); + locals.entry = state.get().lockedOrders.get(locals.slot); if (!locals.entry.active) continue; ++locals.totalActive; @@ -1099,6 +1102,7 @@ struct QSB : public ContractBase struct GetFilledOrders_locals { uint32 i; + uint32 slot; uint32 totalActive; uint32 collected; uint32 effectiveLimit; @@ -1114,9 +1118,11 @@ struct QSB : public ContractBase 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) + // Iterate most-recent-first: start one slot before the next write position + for (locals.i = 0; locals.i < QSB_MAX_FILLED_ORDERS; ++locals.i) { - locals.entry = state.get().filledOrders.get(locals.i); + locals.slot = (state.get().lastFilledOrdersNextOverwriteIdx + QSB_MAX_FILLED_ORDERS - 1 - locals.i) & (QSB_MAX_FILLED_ORDERS - 1); + locals.entry = state.get().filledOrders.get(locals.slot); if (!locals.entry.used) continue; ++locals.totalActive; diff --git a/test/contract_qsb.cpp b/test/contract_qsb.cpp index 5d7ffd5a..280b803d 100644 --- a/test/contract_qsb.cpp +++ b/test/contract_qsb.cpp @@ -771,6 +771,27 @@ TEST(ContractTestingQSB, TestGetLockedOrders_Pagination) EXPECT_EQ(out.returned, 1u); } +TEST(ContractTestingQSB, TestGetLockedOrders_MostRecentFirst) +{ + ContractTestingQSB test; + + const uint64 amount = 1; + increaseEnergy(USER1, amount * 3); + + // Lock 3 orders with nonces 10, 20, 30 (in that order) + test.lock(USER1, amount, 0, 1, 10, ContractTestingQSB::createZeroAddress(), amount); + test.lock(USER1, amount, 0, 1, 20, ContractTestingQSB::createZeroAddress(), amount); + test.lock(USER1, amount, 0, 1, 30, ContractTestingQSB::createZeroAddress(), amount); + + // First page should be most-recent-first: nonce 30, 20, 10 + QSB::GetLockedOrders_output out = test.getLockedOrders(0, 64); + EXPECT_EQ(out.totalActive, 3u); + EXPECT_EQ(out.returned, 3u); + EXPECT_EQ(out.entries.get(0).nonce, 30u); + EXPECT_EQ(out.entries.get(1).nonce, 20u); + EXPECT_EQ(out.entries.get(2).nonce, 10u); +} + TEST(ContractTestingQSB, TestGetFilledOrders_ReturnsEmptyWhenNoFills) { ContractTestingQSB test; @@ -780,6 +801,28 @@ TEST(ContractTestingQSB, TestGetFilledOrders_ReturnsEmptyWhenNoFills) EXPECT_EQ(out.returned, 0u); } +TEST(ContractTestingQSB, TestGetFilledOrders_MostRecentFirst) +{ + ContractTestingQSB test; + + // Insert 3 hashes in order: [0x01], [0x02], [0x03] + for (uint32 i = 1; i <= 3; ++i) + { + QSB::OrderHash hash; + setMemory(hash, 0); + hash.set(0, (uint8)i); + test.getState()->forceMarkOrderFilled(hash); + } + + // First page should be most-recent-first: 0x03, 0x02, 0x01 + QSB::GetFilledOrders_output out = test.getFilledOrders(0, 64); + EXPECT_EQ(out.totalActive, 3u); + EXPECT_EQ(out.returned, 3u); + EXPECT_EQ(out.hashes.get(0).get(0), 3u); + EXPECT_EQ(out.hashes.get(1).get(0), 2u); + EXPECT_EQ(out.hashes.get(2).get(0), 1u); +} + TEST(ContractTestingQSB, TestFilledOrders_RingBufferOverwritesOldEntries) { ContractTestingQSB test; From 3e8dd40ee20928a0c2b21e9de46b82c987dbaf87 Mon Sep 17 00:00:00 2001 From: Jean Date: Wed, 3 Jun 2026 10:54:29 +0200 Subject: [PATCH 17/28] fix(qsb): correct TestLock_RingBufferOverwritesOldestSlot assertions GetLockedOrder searches by nonce, not by slot index. After the ring buffer overwrites slot 0 with a new nonce, GetLockedOrder(0) should return exists=false (nonce 0 is gone). Fix the test to assert this, and also verify the new nonce is findable. Pre-existing failure unrelated to the ordering change. Co-Authored-By: Claude Sonnet 4.6 --- test/contract_qsb.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/contract_qsb.cpp b/test/contract_qsb.cpp index 280b803d..5c629655 100644 --- a/test/contract_qsb.cpp +++ b/test/contract_qsb.cpp @@ -1022,7 +1022,7 @@ TEST(ContractTestingQSB, TestLock_RingBufferOverwritesOldestSlot) ASSERT_TRUE(out.success); } - // Verify slot 0 holds nonce=0 (the first lock). + // Nonce 0 exists before overwrite. QSB::GetLockedOrder_output first = test.getLockedOrder(0); ASSERT_TRUE((bool)first.exists); EXPECT_EQ(first.order.nonce, 0u); @@ -1032,10 +1032,14 @@ TEST(ContractTestingQSB, TestLock_RingBufferOverwritesOldestSlot) QSB::Lock_output overflow = test.lock(USER1, amount, 0, 1, QSB_MAX_LOCKED_ORDERS, ContractTestingQSB::createZeroAddress(), amount); EXPECT_TRUE(overflow.success); - // Slot 0 now holds the newest nonce. - QSB::GetLockedOrder_output overwritten = test.getLockedOrder(0); - ASSERT_TRUE((bool)overwritten.exists); - EXPECT_EQ(overwritten.order.nonce, QSB_MAX_LOCKED_ORDERS); + // Nonce 0 is gone: its slot was overwritten, GetLockedOrder searches by nonce. + QSB::GetLockedOrder_output evicted = test.getLockedOrder(0); + EXPECT_FALSE((bool)evicted.exists); + + // The new nonce is findable. + QSB::GetLockedOrder_output newest = test.getLockedOrder(QSB_MAX_LOCKED_ORDERS); + ASSERT_TRUE((bool)newest.exists); + EXPECT_EQ(newest.order.nonce, QSB_MAX_LOCKED_ORDERS); } TEST(ContractTestingQSB, TestLock_FailsWhenNonceAlreadyUsedAndRefunds) From 14136d49d876e532cc3d557d6e965fe917e33d74 Mon Sep 17 00:00:00 2001 From: Jean Date: Fri, 5 Jun 2026 13:43:10 +0200 Subject: [PATCH 18/28] fix: extend testnet epoch rollover --- src/public_settings.h | 8 ++-- src/qubic.cpp | 108 ++++++++++++++++++++++-------------------- 2 files changed, 61 insertions(+), 55 deletions(-) diff --git a/src/public_settings.h b/src/public_settings.h index 6f730b10..09a85c79 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -147,10 +147,10 @@ static constexpr long long NEURON_VALUE_LIMIT = 1LL; // include commonly needed definitions #include "network_messages/common_def.h" -#ifdef TESTNET -#define TESTNET_EPOCH_DURATION 10000ULL -#define MAX_NUMBER_OF_TICKS_PER_EPOCH (TESTNET_EPOCH_DURATION + 3) -#else +#ifdef TESTNET +#define TESTNET_EPOCH_DURATION 86400ULL +#define MAX_NUMBER_OF_TICKS_PER_EPOCH (TESTNET_EPOCH_DURATION + 3) +#else #define MAX_NUMBER_OF_TICKS_PER_EPOCH (((((60ULL * 60 * 24 * 7 * 1000) / TICK_DURATION_FOR_ALLOCATION_MS) + NUMBER_OF_COMPUTORS - 1) / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS) #endif #define FIRST_TICK_TRANSACTION_OFFSET sizeof(unsigned long long) diff --git a/src/qubic.cpp b/src/qubic.cpp index 6eef3f90..3670427a 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -487,20 +487,31 @@ static inline bool isUsingSwap() #endif } -static bool isLastTickInEpoch() { - const int dayIndex = ::dayIndex(etalonTick.year, etalonTick.month, etalonTick.day); -#ifdef TESTNET - return system.tick - system.initialTick >= TESTNET_EPOCH_DURATION; +static bool isLastTickInEpoch() { + const int dayIndex = ::dayIndex(etalonTick.year, etalonTick.month, etalonTick.day); +#ifdef TESTNET + return system.tick - system.initialTick >= TESTNET_EPOCH_DURATION; #else return (dayIndex == 738570 + system.epoch * 7 && etalonTick.hour >= 12) || dayIndex > 738570 + system.epoch * 7; -#endif -} - -// NOTE: this function doesn't work well on a few CPUs, some bits will be flipped after calling this. It's probably microcode bug. -static void enableAVX() -{ -} +#endif +} + +static inline bool isBootstrappingConfiguredEpochFromScratch() +{ +#if START_NETWORK_FROM_SCRATCH + // Use the configured EPOCH/TICK only for the initial bootstrap. Later + // epochs should follow the seamless transition path automatically. + return system.epoch == EPOCH; +#else + return false; +#endif +} + +// NOTE: this function doesn't work well on a few CPUs, some bits will be flipped after calling this. It's probably microcode bug. +static void enableAVX() +{ +} // Should only be called from tick processor to avoid concurrent state changes, which can cause race conditions as detailed in FIXME below. static void getComputerDigest(m256i& digest) @@ -3205,19 +3216,17 @@ static void processTick(unsigned long long processorNumber) // Here we still let prevDigests == digests of the last tick of last epoch // so that lite client can verify the state of spectrum -#if START_NETWORK_FROM_SCRATCH // only update it if the whole network starts from scratch - // everything starts from files, there is no previous tick of the last epoch - // thus, prevDigests are the digests of the files - if (system.epoch == EPOCH) - { - etalonTick.prevResourceTestingDigest = resourceTestingDigest; - etalonTick.prevSpectrumDigest = spectrumDigests[(SPECTRUM_CAPACITY * 2 - 1) - 1]; - getUniverseDigest(etalonTick.prevUniverseDigest); - getComputerDigest(etalonTick.prevComputerDigest); - etalonTick.prevTransactionBodyDigest = 0; - } -#endif - } + if (isBootstrappingConfiguredEpochFromScratch()) + { + // Everything starts from files, there is no previous tick of the + // last epoch, thus prevDigests are the digests of the files. + etalonTick.prevResourceTestingDigest = resourceTestingDigest; + etalonTick.prevSpectrumDigest = spectrumDigests[(SPECTRUM_CAPACITY * 2 - 1) - 1]; + getUniverseDigest(etalonTick.prevUniverseDigest); + getComputerDigest(etalonTick.prevComputerDigest); + etalonTick.prevTransactionBodyDigest = 0; + } + } else { // it should never go here @@ -4392,10 +4401,8 @@ static void endEpoch() } -#if !START_NETWORK_FROM_SCRATCH - -static bool haveSamePrevDigestsAndTime(const Tick& A, const Tick& B) -{ +static bool haveSamePrevDigestsAndTime(const Tick& A, const Tick& B) +{ return A.prevComputerDigest == B.prevComputerDigest && A.prevResourceTestingDigest == B.prevResourceTestingDigest && A.prevTransactionBodyDigest == B.prevTransactionBodyDigest && @@ -4495,10 +4502,9 @@ static void initializeFirstTick() } _mm_pause(); } -} -#endif - -#if TICK_STORAGE_AUTOSAVE_MODE +} + +#if TICK_STORAGE_AUTOSAVE_MODE // Invalid snapshot data static bool invalidateNodeStates(CHAR16* directory) @@ -6001,15 +6007,15 @@ static void tickProcessor(void*, unsigned long long processorNumber) //const unsigned long long processorNumber = getRunningProcessorID(); -#if !START_NETWORK_FROM_SCRATCH - // only init first tick if it doesn't load all node states from file - if (!loadAllNodeStateFromFile) - { - initializeFirstTick(); - } -#endif - - loadAllNodeStateFromFile = false; + // Only initialize the first tick from peers after the initial configured + // bootstrap epoch. The first local testnet start still begins from the + // configured EPOCH/TICK without requiring prior peers or snapshots. + if (!isBootstrappingConfiguredEpochFromScratch() && !loadAllNodeStateFromFile) + { + initializeFirstTick(); + } + + loadAllNodeStateFromFile = false; unsigned int latestProcessedTick = 0; while (!shutDownNode) { @@ -7090,16 +7096,16 @@ static bool initialize() appendText(message, digestChars); appendText(message, L"."); logToConsole(message); - } - if (!loadContractStateFiles() && (!canObmitLoadNodeState)) - return false; -#if !START_NETWORK_FROM_SCRATCH - if (!loadContractExecFeeFiles() && (!canObmitLoadNodeState)) - return false; -#endif - -#ifdef INCLUDE_CONTRACT_TEST_EXAMPLES - // fill execution fee reserves for test contracts + } + if (!loadContractStateFiles() && (!canObmitLoadNodeState)) + return false; + if (!isBootstrappingConfiguredEpochFromScratch() + && !loadContractExecFeeFiles() + && (!canObmitLoadNodeState)) + return false; + +#ifdef INCLUDE_CONTRACT_TEST_EXAMPLES + // fill execution fee reserves for test contracts setContractFeeReserve(TESTEXA_CONTRACT_INDEX, 100000000000); setContractFeeReserve(TESTEXB_CONTRACT_INDEX, 100000000000); setContractFeeReserve(TESTEXC_CONTRACT_INDEX, 100000000000); From 9c355b13dd5d88e09613f3343990c0cafab5f155 Mon Sep 17 00:00:00 2001 From: Jean Date: Fri, 5 Jun 2026 13:52:42 +0200 Subject: [PATCH 19/28] revert: keep testnet rollover change in settings only --- src/qubic.cpp | 62 +++++++++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/src/qubic.cpp b/src/qubic.cpp index 3670427a..483d4147 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -491,20 +491,9 @@ static bool isLastTickInEpoch() { const int dayIndex = ::dayIndex(etalonTick.year, etalonTick.month, etalonTick.day); #ifdef TESTNET return system.tick - system.initialTick >= TESTNET_EPOCH_DURATION; -#else - return (dayIndex == 738570 + system.epoch * 7 && etalonTick.hour >= 12) - || dayIndex > 738570 + system.epoch * 7; -#endif -} - -static inline bool isBootstrappingConfiguredEpochFromScratch() -{ -#if START_NETWORK_FROM_SCRATCH - // Use the configured EPOCH/TICK only for the initial bootstrap. Later - // epochs should follow the seamless transition path automatically. - return system.epoch == EPOCH; #else - return false; + return (dayIndex == 738570 + system.epoch * 7 && etalonTick.hour >= 12) + || dayIndex > 738570 + system.epoch * 7; #endif } @@ -3216,20 +3205,22 @@ static void processTick(unsigned long long processorNumber) // Here we still let prevDigests == digests of the last tick of last epoch // so that lite client can verify the state of spectrum - if (isBootstrappingConfiguredEpochFromScratch()) + #if START_NETWORK_FROM_SCRATCH // only update it if the whole network starts from scratch + // everything starts from files, there is no previous tick of the last epoch + // thus, prevDigests are the digests of the files + if (system.epoch == EPOCH) { - // Everything starts from files, there is no previous tick of the - // last epoch, thus prevDigests are the digests of the files. etalonTick.prevResourceTestingDigest = resourceTestingDigest; etalonTick.prevSpectrumDigest = spectrumDigests[(SPECTRUM_CAPACITY * 2 - 1) - 1]; getUniverseDigest(etalonTick.prevUniverseDigest); getComputerDigest(etalonTick.prevComputerDigest); etalonTick.prevTransactionBodyDigest = 0; } +#endif } - else - { - // it should never go here + else + { + // it should never go here } // Ensure to only call INITIALIZE and BEGIN_EPOCH once per epoch: @@ -4401,6 +4392,8 @@ static void endEpoch() } +#if !START_NETWORK_FROM_SCRATCH + static bool haveSamePrevDigestsAndTime(const Tick& A, const Tick& B) { return A.prevComputerDigest == B.prevComputerDigest && @@ -4422,9 +4415,9 @@ static void initializeFirstTick() int uniqueVoteCount[NUMBER_OF_COMPUTORS]; int uniqueCount = 0; const unsigned int firstTickIndex = ts.tickToIndexCurrentEpoch(system.initialTick); - while (!shutDownNode) - { - if (broadcastedComputors.computors.epoch == system.epoch) + while (!shutDownNode) + { + if (broadcastedComputors.computors.epoch == system.epoch) { // group ticks with same digest+timestamp and count votes (how many are in each group) setMem(uniqueVoteIndex, sizeof(uniqueVoteIndex), 0); @@ -4500,9 +4493,10 @@ static void initializeFirstTick() } } } - _mm_pause(); - } + _mm_pause(); + } } +#endif #if TICK_STORAGE_AUTOSAVE_MODE @@ -6007,13 +6001,13 @@ static void tickProcessor(void*, unsigned long long processorNumber) //const unsigned long long processorNumber = getRunningProcessorID(); - // Only initialize the first tick from peers after the initial configured - // bootstrap epoch. The first local testnet start still begins from the - // configured EPOCH/TICK without requiring prior peers or snapshots. - if (!isBootstrappingConfiguredEpochFromScratch() && !loadAllNodeStateFromFile) +#if !START_NETWORK_FROM_SCRATCH + // only init first tick if it doesn't load all node states from file + if (!loadAllNodeStateFromFile) { initializeFirstTick(); } +#endif loadAllNodeStateFromFile = false; unsigned int latestProcessedTick = 0; @@ -7099,16 +7093,16 @@ static bool initialize() } if (!loadContractStateFiles() && (!canObmitLoadNodeState)) return false; - if (!isBootstrappingConfiguredEpochFromScratch() - && !loadContractExecFeeFiles() - && (!canObmitLoadNodeState)) +#if !START_NETWORK_FROM_SCRATCH + if (!loadContractExecFeeFiles() && (!canObmitLoadNodeState)) return false; +#endif #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES // fill execution fee reserves for test contracts - setContractFeeReserve(TESTEXA_CONTRACT_INDEX, 100000000000); - setContractFeeReserve(TESTEXB_CONTRACT_INDEX, 100000000000); - setContractFeeReserve(TESTEXC_CONTRACT_INDEX, 100000000000); + setContractFeeReserve(TESTEXA_CONTRACT_INDEX, 100000000000); + setContractFeeReserve(TESTEXB_CONTRACT_INDEX, 100000000000); + setContractFeeReserve(TESTEXC_CONTRACT_INDEX, 100000000000); setContractFeeReserve(TESTEXD_CONTRACT_INDEX, 100000000000); #endif From d6d84aac93b3c2b43be358fb2bbbaae4b621d4d0 Mon Sep 17 00:00:00 2001 From: Jean Date: Mon, 8 Jun 2026 16:52:07 +0200 Subject: [PATCH 20/28] feat: add GGWP contract at index 28, shift QSB to index 29 Co-Authored-By: Claude Sonnet 4.6 --- src/contract_core/contract_def.h | 38 +++++++++++++++++++++----------- src/qubic.cpp | 6 ++--- test/CMakeLists.txt | 3 ++- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 6bec36aa..4bc159a5 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -282,17 +282,27 @@ #undef CONTRACT_STATE_TYPE #undef CONTRACT_STATE2_TYPE -#define ESCROW_CONTRACT_INDEX 27 -#define CONTRACT_INDEX ESCROW_CONTRACT_INDEX -#define CONTRACT_STATE_TYPE ESCROW -#define CONTRACT_STATE2_TYPE ESCROW2 -#include "contracts/Escrow.h" - +#define ESCROW_CONTRACT_INDEX 27 +#define CONTRACT_INDEX ESCROW_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE ESCROW +#define CONTRACT_STATE2_TYPE ESCROW2 +#include "contracts/Escrow.h" + +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define WOLFPACK_CONTRACT_INDEX 28 +#define CONTRACT_INDEX WOLFPACK_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE WOLFPACK +#define CONTRACT_STATE2_TYPE WOLFPACK2 +#include "contracts/GGWP.h" + #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE #undef CONTRACT_STATE2_TYPE -#define QSB_CONTRACT_INDEX 28 +#define QSB_CONTRACT_INDEX 29 #define CONTRACT_INDEX QSB_CONTRACT_INDEX #define CONTRACT_STATE_TYPE QSB #define CONTRACT_STATE2_TYPE QSB2 @@ -409,9 +419,10 @@ constexpr struct ContractDescription {"QTF", 199, 10000, sizeof(QTF::StateData)}, // proposal in epoch 197, IPO in 198, construction and first use in 199 {"QDUEL", 199, 10000, sizeof(QDUEL::StateData)}, // proposal in epoch 197, IPO in 198, construction and first use in 199 {"PULSE", 204, 10000, sizeof(PULSE::StateData)}, // proposal in epoch 202, IPO in 203, construction and first use in 204 - {"VOTTUN", 206, 10000, sizeof(VOTTUNBRIDGE::StateData)}, // proposal in epoch 204, IPO in 205, construction and first use in 206 - {"QUSINO", 208, 10000, sizeof(QUSINO::StateData)}, // proposal in epoch 206, IPO in 207, construction and first use in 208 - {"ESCROW", 210, 10000, sizeof(ESCROW::StateData)}, // proposal in epoch 208, IPO in 209, construction and first use in 210 + {"VOTTUN", 206, 10000, sizeof(VOTTUNBRIDGE::StateData)}, // proposal in epoch 204, IPO in 205, construction and first use in 206 + {"QUSINO", 208, 10000, sizeof(QUSINO::StateData)}, // proposal in epoch 206, IPO in 207, construction and first use in 208 + {"ESCROW", 210, 10000, sizeof(ESCROW::StateData)}, // proposal in epoch 208, IPO in 209, construction and first use in 210 + {"GGWP", 217, 10000, sizeof(WOLFPACK::StateData)}, // proposal in epoch 215, IPO in 216, construction and first use in 217 {"QSB", 212, 10000, sizeof(QSB::StateData)}, // local testnet: constructionEpoch <= testnet epoch to skip IPO // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES @@ -533,9 +544,10 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QTF); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QDUEL); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(PULSE); - REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(VOTTUNBRIDGE); - REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QUSINO); - REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(ESCROW); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(VOTTUNBRIDGE); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QUSINO); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(ESCROW); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(WOLFPACK); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QSB); // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES diff --git a/src/qubic.cpp b/src/qubic.cpp index 8c7f9277..4a619bc3 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -66,10 +66,8 @@ // #define INCLUDE_CONTRACT_TEST_EXAMPLES -#define NO_GGWP - -// contract_def.h needs to be included first to make sure that contracts have minimal access -#include "contract_core/contract_def.h" +// contract_def.h needs to be included first to make sure that contracts have minimal access +#include "contract_core/contract_def.h" #include "contract_core/contract_exec.h" #include diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9f7736c5..37b02161 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -30,7 +30,8 @@ add_executable( assets.cpp common_def.cpp contract_core.cpp - contract_gqmprop.cpp + contract_gqmprop.cpp + contract_ggwp.cpp contract_msvault.cpp contract_nostromo.cpp contract_qbay.cpp From 479cc2df6af05948668f8a7876ae9818d3903a64 Mon Sep 17 00:00:00 2001 From: Jean Date: Mon, 8 Jun 2026 16:55:07 +0200 Subject: [PATCH 21/28] feat: limit QSB overrideLock to 3 attempts per order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add QSB_OVERRIDE_LOCK_MAX_ATTEMPTS=3 and QSBReasonOverrideLimitReached. Store overrideLockCount in LockedOrderEntry (byte +161, within existing padding — struct stays 168 bytes). Block the 4th attempt and add tests for counter increment and the blocked-after-max-attempts path. Co-Authored-By: Claude Sonnet 4.6 --- src/contracts/QubicSolanaBridge.h | 15 +++++++++- test/contract_qsb.cpp | 48 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h index df6b5f33..018d016a 100644 --- a/src/contracts/QubicSolanaBridge.h +++ b/src/contracts/QubicSolanaBridge.h @@ -10,6 +10,7 @@ static constexpr uint32 QSB_MAX_FILLED_ORDERS = 256; // QPI::Array requires powe 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 +static constexpr uint8 QSB_OVERRIDE_LOCK_MAX_ATTEMPTS = 3; // 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). @@ -70,7 +71,8 @@ static constexpr uint8 QSBReasonEraMismatch = 20; static constexpr uint8 QSBReasonInvalidAdmin = 21; static constexpr uint8 QSBReasonInvalidRole = 22; static constexpr uint8 QSBReasonOrderNotFound = 23; -// 21 reserved for future use +static constexpr uint8 QSBReasonOverrideLimitReached = 24; +// 20 reserved for future use struct QSB2 { @@ -141,6 +143,7 @@ struct QSB : public ContractBase uint32 lockEpoch; uint32 orderEra; bit active; + uint8 overrideLockCount; // at +161; 6 bytes padding follow to keep struct at 168 bytes }; // Logging messages @@ -837,6 +840,7 @@ struct QSB : public ContractBase locals.entry.orderHash = output.orderHash; locals.entry.lockEpoch = qpi.epoch(); locals.entry.orderEra = state.get().orderEra; + locals.entry.overrideLockCount = 0; state.mut().lockedOrders.set(state.get().lastLockedOrdersNextOverwriteIdx, locals.entry); state.mut().lastLockedOrdersNextOverwriteIdx = (state.get().lastLockedOrdersNextOverwriteIdx + 1) & (QSB_MAX_LOCKED_ORDERS - 1); @@ -908,6 +912,14 @@ struct QSB : public ContractBase return; } + // Enforce per-order override attempt cap + if (locals.entry.overrideLockCount >= QSB_OVERRIDE_LOCK_MAX_ATTEMPTS) + { + locals.logMsg.reasonCode = QSBReasonOverrideLimitReached; + LOG_INFO(locals.logMsg); + return; + } + // Validate new relayer fee if (input.relayerFee >= locals.entry.amount) { @@ -942,6 +954,7 @@ struct QSB : public ContractBase locals.logMsg.orderHash = locals.entry.orderHash; locals.logMsg.orderEra = locals.entry.orderEra; + locals.entry.overrideLockCount++; state.mut().lockedOrders.set((uint32)locals.idx, locals.entry); output.success = true; copyFromBuffer(locals.logMsg.to, input.toAddress); diff --git a/test/contract_qsb.cpp b/test/contract_qsb.cpp index 5c629655..242b83ee 100644 --- a/test/contract_qsb.cpp +++ b/test/contract_qsb.cpp @@ -1137,6 +1137,54 @@ TEST(ContractTestingQSB, TestOverrideLock_OrderNotFound) EXPECT_FALSE(output.success); } +TEST(ContractTestingQSB, TestOverrideLock_CounterIncrements) +{ + 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); + + // Counter starts at 0 + EXPECT_EQ(test.getLockedOrder(nonce).order.overrideLockCount, 0u); + + // Each successful override increments the counter + EXPECT_TRUE(test.overrideLock(USER1, nonce, 100, ContractTestingQSB::createZeroAddress()).success); + EXPECT_EQ(test.getLockedOrder(nonce).order.overrideLockCount, 1u); + + EXPECT_TRUE(test.overrideLock(USER1, nonce, 200, ContractTestingQSB::createZeroAddress()).success); + EXPECT_EQ(test.getLockedOrder(nonce).order.overrideLockCount, 2u); + + EXPECT_TRUE(test.overrideLock(USER1, nonce, 300, ContractTestingQSB::createZeroAddress()).success); + EXPECT_EQ(test.getLockedOrder(nonce).order.overrideLockCount, 3u); +} + +TEST(ContractTestingQSB, TestOverrideLock_BlockedAfterMaxAttempts) +{ + ContractTestingQSB test; + + const uint64 amount = 1000000; + const uint64 relayerFee = 10000; + const uint32 nonce = 78; + + increaseEnergy(USER1, amount); + test.lock(USER1, amount, relayerFee, 1, nonce, ContractTestingQSB::createZeroAddress(), amount); + + // Three overrides succeed + EXPECT_TRUE(test.overrideLock(USER1, nonce, 100, ContractTestingQSB::createZeroAddress()).success); + EXPECT_TRUE(test.overrideLock(USER1, nonce, 200, ContractTestingQSB::createZeroAddress()).success); + EXPECT_TRUE(test.overrideLock(USER1, nonce, 300, ContractTestingQSB::createZeroAddress()).success); + + // Fourth attempt is blocked + QSB::OverrideLock_output blocked = test.overrideLock(USER1, nonce, 400, ContractTestingQSB::createZeroAddress()); + EXPECT_FALSE(blocked.success); + // Counter must not have advanced + EXPECT_EQ(test.getLockedOrder(nonce).order.overrideLockCount, 3u); +} + // ============================================================================ // Admin Function Tests // ============================================================================ From 216deb765d299e55eb4180d066124ccc9f653b33 Mon Sep 17 00:00:00 2001 From: Jean Date: Mon, 8 Jun 2026 18:05:48 +0200 Subject: [PATCH 22/28] fix: cap Unlock_input signature array to fit 1024-byte limit; document test build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add QSB_MAX_UNLOCK_SIGNATURES=8 so Unlock_input stays ≤1024 bytes (was 6336 bytes with QSB_MAX_ORACLES=64). Update test helper to copy up to the new capacity. Add Running Tests section to README_CLANG.md with the clang-18/Ubuntu linker workaround. Co-Authored-By: Claude Sonnet 4.6 --- README_CLANG.md | 53 +++++++++++++++++++++++++++++++ src/contracts/QubicSolanaBridge.h | 3 +- test/contract_qsb.cpp | 8 +++-- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/README_CLANG.md b/README_CLANG.md index 8ed9fe50..9719a0ce 100644 --- a/README_CLANG.md +++ b/README_CLANG.md @@ -78,3 +78,56 @@ For a example compilation execute the following commands: ``` The output binary will be located at `build/src` + +## Running Tests + +### Configure (once) + +Use a dedicated build directory to avoid mixing test and release artifacts: + +```bash +cd /path/to/core-lite + +cmake -S . -B build-test \ + -DCMAKE_CXX_COMPILER=clang-18 \ + -DCMAKE_BUILD_TYPE=Debug \ + -DBUILD_TESTS=ON \ + -DBUILD_BINARY=OFF \ + -DUSE_SANITIZER=OFF +``` + +### Compile + +```bash +cmake --build build-test --target qubic_core_tests -j$(nproc) +``` + +> **Known linker issue on Ubuntu (clang-18 + GCC stdlib mix):** +> The link step may fail with `undefined reference to '__cxa_pure_virtual'` or +> `vtable for __cxxabiv1::...`. Work around it by appending `-lc++abi -lstdc++` +> manually after compilation completes: +> +> ```bash +> cd build-test/test +> eval "$(cat CMakeFiles/qubic_core_tests.dir/link.txt) -lc++abi -lstdc++" +> ``` +> +> Prerequisites: `sudo apt install libc++abi-dev libstdc++-12-dev` + +### Run all tests + +```bash +./build-test/test/qubic_core_tests +``` + +### Run QSB contract tests only + +```bash +./build-test/test/qubic_core_tests --gtest_filter="ContractTestingQSB*" +``` + +### Run a single test + +```bash +./build-test/test/qubic_core_tests --gtest_filter="ContractTestingQSB.TestOverrideLock_BlockedAfterMaxAttempts" +``` diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h index 018d016a..5f272388 100644 --- a/src/contracts/QubicSolanaBridge.h +++ b/src/contracts/QubicSolanaBridge.h @@ -11,6 +11,7 @@ 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 static constexpr uint8 QSB_OVERRIDE_LOCK_MAX_ATTEMPTS = 3; +static constexpr uint32 QSB_MAX_UNLOCK_SIGNATURES = 8; // max sigs per Unlock call; keeps Unlock_input ≤ 1024 bytes // 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). @@ -293,7 +294,7 @@ struct QSB : public ContractBase { Order order; uint32 numSignatures; - Array signatures; + Array signatures; }; struct Unlock_output diff --git a/test/contract_qsb.cpp b/test/contract_qsb.cpp index 242b83ee..76bbb914 100644 --- a/test/contract_qsb.cpp +++ b/test/contract_qsb.cpp @@ -277,11 +277,13 @@ class ContractTestingQSB : protected ContractTesting { QSB::Unlock_input input; QSB::Unlock_output output; - + input.order = order; input.numSignatures = numSignatures; - copyMemory(input.signatures, signatures); - + uint32 toCopy = numSignatures < QSB_MAX_UNLOCK_SIGNATURES ? numSignatures : QSB_MAX_UNLOCK_SIGNATURES; + for (uint32 i = 0; i < toCopy; i++) + input.signatures.set(i, signatures.get(i)); + invokeUserProcedure(QSB_CONTRACT_INDEX, 3, input, output, user, 0); return output; } From 6e4b54c9167ba37b83b541a9f9d036abc12f20dd Mon Sep 17 00:00:00 2001 From: Jean Date: Wed, 10 Jun 2026 10:13:58 +0200 Subject: [PATCH 23/28] feat(qsb): per-admin proposal cap + clean unreachable guards in executeProposalPayload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add QSB_MAX_PROPOSALS_PER_ADMIN=3: each admin is limited to 3 concurrent active proposals; returns QSBReasonTooManyProposals (33) on overflow. - Add QSBReasonInvalidProposalType (34): Propose now rejects unknown proposal types instead of silently queuing them. - Remove all unreachable guards from executeProposalPayload for AddAdmin, RemoveAdmin, SetAdminThreshold, and EditOracleThreshold — invariants are enforced at Propose time and cancelAllPendingProposals prevents races. - Remove unused `newAdminCount` local variable from executeProposalPayload. - Keep AddRole/RemoveRole execution-time checks (legitimately reachable via idempotency). - 80/80 tests green. Co-Authored-By: Claude Sonnet 4.6 --- src/contracts/QubicSolanaBridge.h | 1064 +++++++++++++++------------- test/contract_qsb.cpp | 1073 +++++++++++++++++------------ 2 files changed, 1234 insertions(+), 903 deletions(-) diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h index 5f272388..69078702 100644 --- a/src/contracts/QubicSolanaBridge.h +++ b/src/contracts/QubicSolanaBridge.h @@ -46,6 +46,26 @@ static constexpr uint32 QSBLogThresholdUpdated = 7; static constexpr uint32 QSBLogRoleGranted = 8; static constexpr uint32 QSBLogRoleRevoked = 9; static constexpr uint32 QSBLogFeeParametersUpdated = 10; +static constexpr uint32 QSBLogProposalCreated = 11; +static constexpr uint32 QSBLogProposalApproved = 12; +static constexpr uint32 QSBLogProposalExecuted = 13; +static constexpr uint32 QSBLogProposalCancelled = 14; + +// Multisig admin constants +static constexpr uint32 QSB_MAX_ADMINS = 8; // approvedMask is uint8; must stay ≤ 8 +static constexpr uint32 QSB_MAX_PROPOSALS = 16; +static constexpr uint32 QSB_MAX_PROPOSALS_PER_ADMIN = 3; +static constexpr uint32 QSB_PROPOSAL_EXPIRY_EPOCHS = 4; // ~4 weeks + +// Proposal types +static constexpr uint8 QSBPropAddAdmin = 1; +static constexpr uint8 QSBPropRemoveAdmin = 2; +static constexpr uint8 QSBPropSetAdminThreshold = 3; +static constexpr uint8 QSBPropAddRole = 4; +static constexpr uint8 QSBPropRemoveRole = 5; +static constexpr uint8 QSBPropEditOracleThreshold = 6; +static constexpr uint8 QSBPropEditFeeParameters = 7; +static constexpr uint8 QSBPropUnpause = 8; // Generic reason codes for logging static constexpr uint8 QSBReasonNone = 0; @@ -73,7 +93,17 @@ static constexpr uint8 QSBReasonInvalidAdmin = 21; static constexpr uint8 QSBReasonInvalidRole = 22; static constexpr uint8 QSBReasonOrderNotFound = 23; static constexpr uint8 QSBReasonOverrideLimitReached = 24; -// 20 reserved for future use +// Multisig admin reason codes +static constexpr uint8 QSBReasonProposalNotFound = 25; +static constexpr uint8 QSBReasonProposalExpired = 26; +static constexpr uint8 QSBReasonAlreadyApproved = 27; +static constexpr uint8 QSBReasonProposalFull = 28; +static constexpr uint8 QSBReasonWouldLockContract = 29; +static constexpr uint8 QSBReasonNotProposer = 30; +static constexpr uint8 QSBReasonAlreadyAdmin = 31; +static constexpr uint8 QSBReasonAdminFull = 32; +static constexpr uint8 QSBReasonTooManyProposals = 33; +static constexpr uint8 QSBReasonInvalidProposalType = 34; struct QSB2 { @@ -254,6 +284,44 @@ struct QSB : public ContractBase sint8 _terminator; }; + struct QSBLogProposalMessage + { + uint32 _contractIndex; + uint32 _type; + uint8 proposalId; + uint8 proposalType; + id proposer; + id actor; + uint8 approvalCount; + uint8 success; + uint8 reasonCode; + sint8 _terminator; + }; + + // Union-style: fields used depend on proposalType. Unused fields are zero. + struct AdminProposal + { + uint8 proposalType; // QSBProp* constant + uint8 active; // 1 = slot in use + uint8 executed; // 1 = executed successfully + + id proposer; // admin who created this proposal + uint32 createdEpoch; // for expiry: createdEpoch + QSB_PROPOSAL_EXPIRY_EPOCHS + + uint8 approvalCount; // cached popcount of approvedMask + uint8 approvedMask; // bit i = admins[i] approved (max 8 admins) + + // Payload — fields used depend on proposalType + id targetId; // AddAdmin, RemoveAdmin, AddRole/RemoveRole account + uint8 role; // AddRole, RemoveRole: (uint8)Role::Oracle or Role::Pauser + uint8 newAdminThreshold; // SetAdminThreshold + uint8 newOracleThreshold; // EditOracleThreshold + id protocolFeeRecipient; + id oracleFeeRecipient; + uint32 bpsFee; + uint32 protocolFee; + }; + // --------------------------------------------------------------------- // User-facing I/O structures // --------------------------------------------------------------------- @@ -377,6 +445,42 @@ struct QSB : public ContractBase bit success; }; + // Propose: create a typed admin proposal (proposer auto-approves) + struct Propose_input + { + uint8 proposalType; + id targetId; + uint8 role; + uint8 newAdminThreshold; + uint8 newOracleThreshold; + id protocolFeeRecipient; + id oracleFeeRecipient; + uint32 bpsFee; + uint32 protocolFee; + }; + struct Propose_output + { + uint8 proposalId; // slot index; valid only when success == true + bit success; + uint8 reasonCode; + }; + + struct ApproveProposal_input { uint8 proposalId; }; + struct ApproveProposal_output { bit success; bit executed; uint8 reasonCode; }; + + struct CancelProposal_input { uint8 proposalId; }; + struct CancelProposal_output { bit success; uint8 reasonCode; }; + + struct GetProposal_input { uint8 proposalId; }; + struct GetProposal_output { bit exists; AdminProposal proposal; }; + + struct GetProposals_input {}; + struct GetProposals_output + { + uint8 count; + Array proposals; + }; + // --------------------------------------------------------------------- // View / frontend helper functions // --------------------------------------------------------------------- @@ -387,15 +491,17 @@ struct QSB : public ContractBase struct GetConfig_output { - id admin; - id protocolFeeRecipient; - id oracleFeeRecipient; + uint8 adminCount; + uint8 adminThreshold; + Array admins; + id protocolFeeRecipient; + id oracleFeeRecipient; uint32 bpsFee; uint32 protocolFee; uint32 oracleCount; uint32 pauserCount; - uint8 oracleThreshold; - bit paused; + uint8 oracleThreshold; + bit paused; uint32 orderEra; }; @@ -506,9 +612,14 @@ struct QSB : public ContractBase // --------------------------------------------------------------------- struct StateData { - id admin; - id protocolFeeRecipient; // receives protocolFeeAmount - id oracleFeeRecipient; // receives oracleFeeAmount + // Multisig admin (replaces single `id admin`) + Array admins; // zero entry = empty slot + uint8 adminCount; // number of active admins + uint8 adminThreshold; // M in M-of-N (always ≥ 1, always ≤ adminCount) + Array proposals; + + id protocolFeeRecipient; + id oracleFeeRecipient; Array oracles; Array pausers; Array filledOrders; @@ -518,10 +629,10 @@ struct QSB : public ContractBase uint32 lastLockedOrdersNextOverwriteIdx; 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 bpsFee; + uint32 protocolFee; + uint8 oracleThreshold; // percent [1..100] + bit paused; uint32 orderEra; }; @@ -581,18 +692,43 @@ struct QSB : public ContractBase 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) + // Popcount for uint8 approvedMask (used by multisig approval tracking) + inline static uint8 countBitsUint8(uint8 mask, uint8 i) { - if (isZero(state.get().admin)) - return true; - return who == state.get().admin; + uint8 count = 0; + for (i = 0; i < 8; ++i) + { + if (mask & (uint8)(1u << i)) ++count; + } + return count; + } + + // Check if caller is in the admin array + inline static bool isAdmin(const QPI::ContractState& state, const id& who, uint32 i) + { + for (i = 0; i < QSB_MAX_ADMINS; ++i) + { + if (!isZero(state.get().admins.get(i)) && state.get().admins.get(i) == who) + return true; + } + return false; + } + + // Find admin slot index; returns NULL_INDEX if not found + inline static sint64 findAdminIndex(const QPI::ContractState& state, const id& who, uint32 i) + { + for (i = 0; i < QSB_MAX_ADMINS; ++i) + { + if (!isZero(state.get().admins.get(i)) && state.get().admins.get(i) == who) + return (sint64)i; + } + return NULL_INDEX; } // 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)) + if (isAdmin(state, who, 0)) return true; for (i = 0; i < state.get().pausers.capacity(); ++i) @@ -603,6 +739,144 @@ struct QSB : public ContractBase return false; } + // Cancel all pending (active) proposals — called when admin set changes + inline static void cancelAllPendingProposals(QPI::ContractState& state, uint32 i) + { + AdminProposal prop; + for (i = 0; i < QSB_MAX_PROPOSALS; ++i) + { + prop = state.get().proposals.get(i); + if (prop.active) + { + prop.active = 0; + state.mut().proposals.set(i, prop); + } + } + } + + // Execute the payload of an approved proposal. Returns true on success. + // Pure state mutation — no qpi access. + inline static bool executeProposalPayload(QPI::ContractState& state, const AdminProposal& prop, uint32 i) + { + RoleEntry entry; + sint64 idx; + + if (prop.proposalType == QSBPropAddAdmin) + { + for (i = 0; i < QSB_MAX_ADMINS; ++i) + { + if (isZero(state.get().admins.get(i))) + { + state.mut().admins.set(i, prop.targetId); + state.mut().adminCount = state.get().adminCount + 1; + return true; + } + } + return false; + } + else if (prop.proposalType == QSBPropRemoveAdmin) + { + idx = findAdminIndex(state, prop.targetId, 0); + state.mut().admins.set((uint32)idx, NULL_ID); + state.mut().adminCount = state.get().adminCount - 1; + return true; + } + else if (prop.proposalType == QSBPropSetAdminThreshold) + { + state.mut().adminThreshold = prop.newAdminThreshold; + return true; + } + else if (prop.proposalType == QSBPropAddRole) + { + if (prop.role == (uint8)Role::Oracle) + { + if (findOracleIndex(state, prop.targetId, 0) != NULL_INDEX) + return true; + for (i = 0; i < state.get().oracles.capacity(); ++i) + { + entry = state.get().oracles.get(i); + if (!entry.active) + { + entry.account = prop.targetId; + entry.active = true; + state.mut().oracles.set(i, entry); + ++state.mut().oracleCount; + return true; + } + } + return false; + } + else if (prop.role == (uint8)Role::Pauser) + { + if (findPauserIndex(state, prop.targetId, 0) != NULL_INDEX) + return true; + for (i = 0; i < state.get().pausers.capacity(); ++i) + { + entry = state.get().pausers.get(i); + if (!entry.active) + { + entry.account = prop.targetId; + entry.active = true; + state.mut().pausers.set(i, entry); + ++state.mut().pauserCount; + return true; + } + } + return false; + } + return false; + } + else if (prop.proposalType == QSBPropRemoveRole) + { + if (prop.role == (uint8)Role::Oracle) + { + idx = findOracleIndex(state, prop.targetId, 0); + if (idx == NULL_INDEX) + return true; + entry = state.get().oracles.get((uint32)idx); + entry.active = false; + state.mut().oracles.set((uint32)idx, entry); + if (state.get().oracleCount > 0) --state.mut().oracleCount; + return true; + } + else if (prop.role == (uint8)Role::Pauser) + { + idx = findPauserIndex(state, prop.targetId, 0); + if (idx == NULL_INDEX) + return true; + entry = state.get().pausers.get((uint32)idx); + entry.active = false; + state.mut().pausers.set((uint32)idx, entry); + if (state.get().pauserCount > 0) --state.mut().pauserCount; + return true; + } + return false; + } + else if (prop.proposalType == QSBPropEditOracleThreshold) + { + state.mut().oracleThreshold = prop.newOracleThreshold; + return true; + } + else if (prop.proposalType == QSBPropEditFeeParameters) + { + if (prop.bpsFee != 0 && prop.bpsFee <= QSB_MAX_BPS_FEE) + state.mut().bpsFee = prop.bpsFee; + if (prop.protocolFee != 0 && prop.protocolFee <= QSB_MAX_PROTOCOL_FEE) + state.mut().protocolFee = prop.protocolFee; + if (!isZero(prop.protocolFeeRecipient)) + state.mut().protocolFeeRecipient = prop.protocolFeeRecipient; + if (!isZero(prop.oracleFeeRecipient)) + state.mut().oracleFeeRecipient = prop.oracleFeeRecipient; + return true; + } + else if (prop.proposalType == QSBPropUnpause) + { + state.mut().paused = false; + 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) { @@ -970,16 +1244,44 @@ struct QSB : public ContractBase // View helpers PUBLIC_FUNCTION(GetConfig) { - output.admin = state.get().admin; + output.adminCount = state.get().adminCount; + output.adminThreshold = state.get().adminThreshold; + output.admins = state.get().admins; 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.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; + output.paused = state.get().paused; + output.orderEra = state.get().orderEra; + } + + PUBLIC_FUNCTION(GetProposal) + { + output.exists = false; + if (input.proposalId < QSB_MAX_PROPOSALS) + { + output.proposal = state.get().proposals.get(input.proposalId); + output.exists = output.proposal.active; + } + } + + struct GetProposals_locals { uint32 i; AdminProposal prop; }; + PUBLIC_FUNCTION_WITH_LOCALS(GetProposals) + { + output.count = 0; + setMemory(output.proposals, 0); + for (locals.i = 0; locals.i < QSB_MAX_PROPOSALS; ++locals.i) + { + locals.prop = state.get().proposals.get(locals.i); + if (locals.prop.active) + { + output.proposals.set(output.count, locals.prop); + ++output.count; + } + } } PUBLIC_FUNCTION(IsOracle) @@ -1436,396 +1738,285 @@ struct QSB : public ContractBase } // --------------------------------------------------------------------- - // Admin procedures + // Admin procedures (multisig) // --------------------------------------------------------------------- - struct TransferAdmin_locals + struct Propose_locals { - QSBLogAdminTransferredMessage logMsg; + sint64 adminIdx; + uint32 i; + uint8 slotIdx; + uint8 adminProposalCount; + AdminProposal prop; + bool execOk; + QSBLogProposalMessage logMsg; }; - PUBLIC_PROCEDURE_WITH_LOCALS(TransferAdmin) + PUBLIC_PROCEDURE_WITH_LOCALS(Propose) { - 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; + output.success = false; + output.proposalId = 0; + output.reasonCode = QSBReasonNone; - // 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; - } - - if (isZero(input.newAdmin)) - { - locals.logMsg.reasonCode = QSBReasonInvalidAdmin; - LOG_INFO(locals.logMsg); - return; + locals.adminIdx = findAdminIndex(state, qpi.invocator(), 0); + if (locals.adminIdx == NULL_INDEX) + { output.reasonCode = QSBReasonNotAdmin; return; } + + if (input.proposalType == 0 || input.proposalType > QSBPropUnpause) + { output.reasonCode = QSBReasonInvalidRole; return; } + + // Per-type payload validation + if (input.proposalType == QSBPropAddAdmin) + { + if (isZero(input.targetId)) + { output.reasonCode = QSBReasonInvalidAdmin; return; } + if (findAdminIndex(state, input.targetId, 0) != NULL_INDEX) + { output.reasonCode = QSBReasonAlreadyAdmin; return; } + if (state.get().adminCount >= QSB_MAX_ADMINS) + { output.reasonCode = QSBReasonAdminFull; return; } + } + else if (input.proposalType == QSBPropRemoveAdmin) + { + if (isZero(input.targetId)) + { output.reasonCode = QSBReasonInvalidAdmin; return; } + if (findAdminIndex(state, input.targetId, 0) == NULL_INDEX) + { output.reasonCode = QSBReasonRoleMissing; return; } + if (state.get().adminCount <= 1) + { output.reasonCode = QSBReasonWouldLockContract; return; } + if ((state.get().adminCount - 1) < state.get().adminThreshold) + { output.reasonCode = QSBReasonWouldLockContract; return; } + } + else if (input.proposalType == QSBPropSetAdminThreshold) + { + if (input.newAdminThreshold == 0 || input.newAdminThreshold > state.get().adminCount) + { output.reasonCode = QSBReasonInvalidThreshold; return; } + } + else if (input.proposalType == QSBPropAddRole || input.proposalType == QSBPropRemoveRole) + { + if (isZero(input.targetId)) + { output.reasonCode = QSBReasonInvalidAdmin; return; } + if (input.role != (uint8)Role::Oracle && input.role != (uint8)Role::Pauser) + { output.reasonCode = QSBReasonInvalidRole; return; } + } + else if (input.proposalType == QSBPropEditOracleThreshold) + { + if (input.newOracleThreshold == 0 || input.newOracleThreshold > 100) + { output.reasonCode = QSBReasonInvalidThreshold; return; } + } + else if (input.proposalType == QSBPropEditFeeParameters) + { + if (input.bpsFee > QSB_MAX_BPS_FEE || input.protocolFee > QSB_MAX_PROTOCOL_FEE) + { output.reasonCode = QSBReasonInvalidFeeParams; return; } + } + else if (input.proposalType != QSBPropUnpause) + { output.reasonCode = QSBReasonInvalidProposalType; return; } + + // Enforce per-admin concurrent proposal cap + locals.adminProposalCount = 0; + for (locals.i = 0; locals.i < QSB_MAX_PROPOSALS; ++locals.i) + { + locals.prop = state.get().proposals.get(locals.i); + if (locals.prop.active && locals.prop.proposer == qpi.invocator()) + ++locals.adminProposalCount; + } + if (locals.adminProposalCount >= QSB_MAX_PROPOSALS_PER_ADMIN) + { output.reasonCode = QSBReasonTooManyProposals; return; } + + // Find free proposal slot + locals.slotIdx = (uint8)QSB_MAX_PROPOSALS; + for (locals.i = 0; locals.i < QSB_MAX_PROPOSALS; ++locals.i) + { + if (!state.get().proposals.get(locals.i).active) + { locals.slotIdx = (uint8)locals.i; break; } + } + if (locals.slotIdx >= QSB_MAX_PROPOSALS) + { output.reasonCode = QSBReasonProposalFull; return; } + + // Build proposal; proposer auto-approves + setMemory(locals.prop, 0); + locals.prop.proposalType = input.proposalType; + locals.prop.active = 1; + locals.prop.executed = 0; + locals.prop.proposer = qpi.invocator(); + locals.prop.createdEpoch = qpi.epoch(); + locals.prop.approvedMask = (uint8)(1u << (uint8)locals.adminIdx); + locals.prop.approvalCount = 1; + locals.prop.targetId = input.targetId; + locals.prop.role = input.role; + locals.prop.newAdminThreshold = input.newAdminThreshold; + locals.prop.newOracleThreshold = input.newOracleThreshold; + locals.prop.protocolFeeRecipient = input.protocolFeeRecipient; + locals.prop.oracleFeeRecipient = input.oracleFeeRecipient; + locals.prop.bpsFee = input.bpsFee; + locals.prop.protocolFee = input.protocolFee; + state.mut().proposals.set(locals.slotIdx, locals.prop); + output.proposalId = locals.slotIdx; + output.success = true; + + // Execute immediately when threshold == 1 (single-admin or bootstrap mode) + if (state.get().adminThreshold <= 1) + { + locals.execOk = executeProposalPayload(state, locals.prop, 0); + locals.prop = state.get().proposals.get(locals.slotIdx); + locals.prop.active = 0; + locals.prop.executed = locals.execOk ? 1 : 0; + state.mut().proposals.set(locals.slotIdx, locals.prop); + if (locals.execOk && + (input.proposalType == QSBPropAddAdmin || + input.proposalType == QSBPropRemoveAdmin || + input.proposalType == QSBPropSetAdminThreshold)) + { + cancelAllPendingProposals(state, 0); + } + output.success = locals.execOk; } - state.mut().admin = input.newAdmin; - output.success = true; - locals.logMsg.success = 1; + locals.logMsg._contractIndex = SELF_INDEX; + locals.logMsg._type = QSBLogProposalCreated; + locals.logMsg.proposalId = output.proposalId; + locals.logMsg.proposalType = input.proposalType; + locals.logMsg.proposer = qpi.invocator(); + locals.logMsg.actor = qpi.invocator(); + locals.logMsg.approvalCount = 1; + locals.logMsg.success = output.success ? 1 : 0; + locals.logMsg.reasonCode = output.reasonCode; + locals.logMsg._terminator = 0; LOG_INFO(locals.logMsg); } - struct EditOracleThreshold_locals + struct ApproveProposal_locals { - QSBLogThresholdUpdatedMessage logMsg; + sint64 adminIdx; + uint8 bitPos; + uint8 propType; + AdminProposal prop; + bool execOk; + QSBLogProposalMessage logMsg; }; - PUBLIC_PROCEDURE_WITH_LOCALS(EditOracleThreshold) + PUBLIC_PROCEDURE_WITH_LOCALS(ApproveProposal) { - output.success = false; - output.oldThreshold = state.get().oracleThreshold; + output.success = false; + output.executed = false; + output.reasonCode = QSBReasonNone; 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; - } + locals.adminIdx = findAdminIndex(state, qpi.invocator(), 0); + if (locals.adminIdx == NULL_INDEX) + { output.reasonCode = QSBReasonNotAdmin; 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; - } + if (input.proposalId >= QSB_MAX_PROPOSALS) + { output.reasonCode = QSBReasonProposalNotFound; 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); - } + locals.prop = state.get().proposals.get(input.proposalId); - struct AddRole_locals - { - RoleEntry entry; - uint32 i; - QSBLogRoleMessage logMsg; - }; + if (!locals.prop.active) + { output.reasonCode = QSBReasonProposalNotFound; return; } - PUBLIC_PROCEDURE_WITH_LOCALS(AddRole) - { - output.success = false; - - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - - if (!isAdmin(state, qpi.invocator())) + if (qpi.epoch() > locals.prop.createdEpoch + QSB_PROPOSAL_EXPIRY_EPOCHS) { - 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); + locals.prop.active = 0; + state.mut().proposals.set(input.proposalId, locals.prop); + output.reasonCode = QSBReasonProposalExpired; 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; - } + locals.bitPos = (uint8)locals.adminIdx; + if (locals.bitPos < 8 && (locals.prop.approvedMask & (uint8)(1u << locals.bitPos))) + { output.reasonCode = QSBReasonAlreadyApproved; 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; - } - } - 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 = QSBReasonNoSpace; - 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; - } + locals.prop.approvedMask |= (uint8)(1u << locals.bitPos); + locals.prop.approvalCount = countBitsUint8(locals.prop.approvedMask, 0); + state.mut().proposals.set(input.proposalId, locals.prop); + output.success = true; - for (locals.i = 0; locals.i < state.get().pausers.capacity(); ++locals.i) + if (locals.prop.approvalCount >= state.get().adminThreshold) + { + locals.propType = locals.prop.proposalType; + locals.execOk = executeProposalPayload(state, locals.prop, 0); + locals.prop = state.get().proposals.get(input.proposalId); + locals.prop.active = 0; + locals.prop.executed = locals.execOk ? 1 : 0; + state.mut().proposals.set(input.proposalId, locals.prop); + output.executed = true; + if (locals.execOk && + (locals.propType == QSBPropAddAdmin || + locals.propType == QSBPropRemoveAdmin || + locals.propType == QSBPropSetAdminThreshold)) { - 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; - } + cancelAllPendingProposals(state, 0); } - 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 = QSBReasonNoSpace; - locals.logMsg._terminator = 0; - LOG_INFO(locals.logMsg); - return; - } - else - { - 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 = QSBReasonInvalidRole; - locals.logMsg._terminator = 0; - LOG_INFO(locals.logMsg); - return; } + + locals.logMsg._contractIndex = SELF_INDEX; + locals.logMsg._type = output.executed ? QSBLogProposalExecuted : QSBLogProposalApproved; + locals.logMsg.proposalId = input.proposalId; + locals.logMsg.proposalType = locals.prop.proposalType; + locals.logMsg.proposer = locals.prop.proposer; + locals.logMsg.actor = qpi.invocator(); + locals.logMsg.approvalCount = locals.prop.approvalCount; + locals.logMsg.success = output.success ? 1 : 0; + locals.logMsg.reasonCode = output.reasonCode; + locals.logMsg._terminator = 0; + LOG_INFO(locals.logMsg); } - struct RemoveRole_locals + struct CancelProposal_locals { - RoleEntry entry; - sint64 idx; - QSBLogRoleMessage logMsg; + AdminProposal prop; + QSBLogProposalMessage logMsg; }; - PUBLIC_PROCEDURE_WITH_LOCALS(RemoveRole) + PUBLIC_PROCEDURE_WITH_LOCALS(CancelProposal) { - output.success = false; + output.success = false; + output.reasonCode = QSBReasonNone; 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 (!isAdmin(state, qpi.invocator(), 0)) + { output.reasonCode = QSBReasonNotAdmin; 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); - } - } - } + if (input.proposalId >= QSB_MAX_PROPOSALS) + { output.reasonCode = QSBReasonProposalNotFound; return; } - struct Pause_locals - { - QSBLogPausedMessage logMsg; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(Pause) - { - output.success = false; + locals.prop = state.get().proposals.get(input.proposalId); - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } + if (!locals.prop.active) + { output.reasonCode = QSBReasonProposalNotFound; return; } - 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; - } + if (locals.prop.proposer != qpi.invocator()) + { output.reasonCode = QSBReasonNotProposer; return; } - state.mut().paused = true; + locals.prop.active = 0; + state.mut().proposals.set(input.proposalId, locals.prop); 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; + locals.logMsg._type = QSBLogProposalCancelled; + locals.logMsg.proposalId = input.proposalId; + locals.logMsg.proposalType = locals.prop.proposalType; + locals.logMsg.proposer = locals.prop.proposer; + locals.logMsg.actor = qpi.invocator(); + locals.logMsg.approvalCount = locals.prop.approvalCount; + locals.logMsg.success = 1; + locals.logMsg.reasonCode = QSBReasonNone; + locals.logMsg._terminator = 0; LOG_INFO(locals.logMsg); } - struct Unpause_locals + struct Pause_locals { QSBLogPausedMessage logMsg; }; - PUBLIC_PROCEDURE_WITH_LOCALS(Unpause) + PUBLIC_PROCEDURE_WITH_LOCALS(Pause) { output.success = false; @@ -1834,23 +2025,23 @@ struct QSB : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - if (!isAdmin(state, qpi.invocator())) + if (!isAdminOrPauser(state, qpi.invocator(), 0)) // Pause stays single-key (emergency brake) { locals.logMsg._contractIndex = SELF_INDEX; - locals.logMsg._type = QSBLogUnpaused; + locals.logMsg._type = QSBLogPaused; locals.logMsg.caller = qpi.invocator(); locals.logMsg.success = 0; - locals.logMsg.reasonCode = QSBReasonNotAdmin; + locals.logMsg.reasonCode = QSBReasonNotAdminOrPauser; locals.logMsg._terminator = 0; LOG_INFO(locals.logMsg); return; } - state.mut().paused = false; + state.mut().paused = true; output.success = true; locals.logMsg._contractIndex = SELF_INDEX; - locals.logMsg._type = QSBLogUnpaused; + locals.logMsg._type = QSBLogPaused; locals.logMsg.caller = qpi.invocator(); locals.logMsg.success = 1; locals.logMsg.reasonCode = QSBReasonNone; @@ -1858,101 +2049,6 @@ struct QSB : public ContractBase 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 @@ -1966,20 +2062,21 @@ struct QSB : public ContractBase REGISTER_USER_FUNCTION(GetPausers, 8); REGISTER_USER_FUNCTION(GetLockedOrders, 9); REGISTER_USER_FUNCTION(GetFilledOrders, 10); + REGISTER_USER_FUNCTION(GetProposal, 11); + REGISTER_USER_FUNCTION(GetProposals, 12); // 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); + // Emergency pause — single-key, any admin or pauser REGISTER_USER_PROCEDURE(Pause, 14); - REGISTER_USER_PROCEDURE(Unpause, 15); - REGISTER_USER_PROCEDURE(EditFeeParameters, 16); + + // Multisig admin procedures + REGISTER_USER_PROCEDURE(Propose, 20); + REGISTER_USER_PROCEDURE(ApproveProposal, 21); + REGISTER_USER_PROCEDURE(CancelProposal, 22); } // --------------------------------------------------------------------- @@ -1988,12 +2085,23 @@ struct QSB : public ContractBase struct END_EPOCH_locals { - // No periodic processing required in the current bridge design. + uint32 i; + AdminProposal prop; }; END_EPOCH_WITH_LOCALS() { - // Intentionally left empty. + // Sweep expired proposals + for (locals.i = 0; locals.i < QSB_MAX_PROPOSALS; ++locals.i) + { + locals.prop = state.get().proposals.get(locals.i); + if (locals.prop.active && + qpi.epoch() > locals.prop.createdEpoch + QSB_PROPOSAL_EXPIRY_EPOCHS) + { + locals.prop.active = 0; + state.mut().proposals.set(locals.i, locals.prop); + } + } } // --------------------------------------------------------------------- @@ -2002,29 +2110,35 @@ struct QSB : public ContractBase INITIALIZE() { - // No admin set initially; first TransferAdmin call bootstraps admin. - // Admin = SINUBYSBZKBSVEFQDZBQWUEJWRXCXOZNKPHIXDZWRBKXDSPJEHFAMBACXHUN (.temp/qubic-admin.keys.json) - state.mut().admin = id(11994886480163374182ULL, 7222723150474050185ULL, 4187743050690849231ULL, 4967671197750064684ULL); + // Multisig admin setup — 2-of-2 from deployment. + // Replace both keys with real production keys before mainnet deployment. + // Admin 0: id(100, 200, 300, 400) — test key, matches ADMIN in contract_qsb.cpp + // Admin 1: id(101, 201, 301, 401) — test key, matches ADMIN2 in contract_qsb.cpp + setMemory(state.mut().admins, 0); + state.mut().admins.set(0, id(100ULL, 200ULL, 300ULL, 400ULL)); + state.mut().admins.set(1, id(101ULL, 201ULL, 301ULL, 401ULL)); + state.mut().adminCount = 2; + state.mut().adminThreshold = 2; + setMemory(state.mut().proposals, 0); + state.mut().paused = false; - state.mut().oracleThreshold = 67; // default 67% (2/3 + 1 style) + state.mut().oracleThreshold = 67; state.mut().lastFilledOrdersNextOverwriteIdx = 0; state.mut().lastLockedOrdersNextOverwriteIdx = 0; state.mut().oracleCount = 0; - state.mut().pauserCount = 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().filledOrdersPrev, 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().bpsFee = 0; + state.mut().protocolFee = 0; state.mut().protocolFeeRecipient = NULL_ID; - state.mut().oracleFeeRecipient = NULL_ID; + state.mut().oracleFeeRecipient = NULL_ID; state.mut().orderEra = 0; } diff --git a/test/contract_qsb.cpp b/test/contract_qsb.cpp index 76bbb914..5c872aed 100644 --- a/test/contract_qsb.cpp +++ b/test/contract_qsb.cpp @@ -7,6 +7,7 @@ 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 ADMIN2(101, 201, 301, 401); // second admin, matches INITIALIZE slot 1 static const id ORACLE1(500, 600, 700, 800); static const id ORACLE2(900, 1000, 1100, 1200); static const id ORACLE3(1300, 1400, 1500, 1600); @@ -24,9 +25,34 @@ class StateCheckerQSB : public QSB, public QSB::StateData return *reinterpret_cast*>(static_cast(this)); } - void checkAdmin(const id& expectedAdmin) const + void checkAdminCount(uint8 expected) const { - EXPECT_EQ(this->admin, expectedAdmin); + EXPECT_EQ(this->adminCount, expected); + } + + void checkAdminThreshold(uint8 expected) const + { + EXPECT_EQ(this->adminThreshold, expected); + } + + void checkIsAdmin(const id& who) const + { + bool found = false; + for (uint32 i = 0; i < QSB_MAX_ADMINS; ++i) + { + if (this->admins.get(i) == who) { found = true; break; } + } + EXPECT_TRUE(found); + } + + void checkIsNotAdmin(const id& who) const + { + bool found = false; + for (uint32 i = 0; i < QSB_MAX_ADMINS; ++i) + { + if (this->admins.get(i) == who) { found = true; break; } + } + EXPECT_FALSE(found); } void checkPaused(bool expectedPaused) const @@ -98,12 +124,7 @@ class ContractTestingQSB : protected ContractTesting initEmptyUniverse(); INIT_CONTRACT(QSB); callSystemProcedure(QSB_CONTRACT_INDEX, INITIALIZE); - - // INITIALIZE sets admin to the real deployment key; transfer to test ADMIN. - static const id DEPLOY_ADMIN(11994886480163374182ULL, 7222723150474050185ULL, 4187743050690849231ULL, 4967671197750064684ULL); - increaseEnergy(DEPLOY_ADMIN, 1); - transferAdmin(DEPLOY_ADMIN, ADMIN); - + // INITIALIZE sets ADMIN (slot 0) and ADMIN2 (slot 1) with threshold=2. checkContractExecCleanup(); } @@ -249,13 +270,13 @@ class ContractTestingQSB : protected ContractTesting { 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; } @@ -264,11 +285,11 @@ class ContractTestingQSB : protected ContractTesting { 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; } @@ -289,94 +310,141 @@ class ContractTestingQSB : protected ContractTesting } // ============================================================================ - // Admin Procedure Helpers + // Admin Procedure Helpers (proposal system) // ============================================================================ - QSB::TransferAdmin_output transferAdmin(const id& user, const id& newAdmin) + QSB::Pause_output pause(const id& user) { - QSB::TransferAdmin_input input; - QSB::TransferAdmin_output output; - - input.newAdmin = newAdmin; - - invokeUserProcedure(QSB_CONTRACT_INDEX, 10, input, output, user, 0); + QSB::Pause_input input; + QSB::Pause_output output; + increaseEnergy(user, 1); + invokeUserProcedure(QSB_CONTRACT_INDEX, 14, input, output, user, 0); return output; } - QSB::EditOracleThreshold_output editOracleThreshold(const id& user, uint8 newThreshold) + QSB::Propose_output propose(const id& user, const QSB::Propose_input& input) { - QSB::EditOracleThreshold_input input; - QSB::EditOracleThreshold_output output; - - input.newThreshold = newThreshold; - - invokeUserProcedure(QSB_CONTRACT_INDEX, 11, input, output, user, 0); + QSB::Propose_output output; + increaseEnergy(user, 1); + invokeUserProcedure(QSB_CONTRACT_INDEX, 20, input, output, user, 0); return output; } - QSB::AddRole_output addRole(const id& user, uint8 role, const id& account) + QSB::ApproveProposal_output approveProposal(const id& user, uint8 proposalId) { - QSB::AddRole_input input; - QSB::AddRole_output output; - - input.role = role; - input.account = account; - - invokeUserProcedure(QSB_CONTRACT_INDEX, 12, input, output, user, 0); + QSB::ApproveProposal_input input; + QSB::ApproveProposal_output output; + input.proposalId = proposalId; + increaseEnergy(user, 1); + invokeUserProcedure(QSB_CONTRACT_INDEX, 21, input, output, user, 0); return output; } - QSB::RemoveRole_output removeRole(const id& user, uint8 role, const id& account) + QSB::CancelProposal_output cancelProposal(const id& user, uint8 proposalId) { - QSB::RemoveRole_input input; - QSB::RemoveRole_output output; - - input.role = role; - input.account = account; - - invokeUserProcedure(QSB_CONTRACT_INDEX, 13, input, output, user, 0); + QSB::CancelProposal_input input; + QSB::CancelProposal_output output; + input.proposalId = proposalId; + increaseEnergy(user, 1); + invokeUserProcedure(QSB_CONTRACT_INDEX, 22, input, output, user, 0); return output; } - QSB::Pause_output pause(const id& user) + // Propose-then-approve convenience wrapper for 2-of-2 operations. + // Returns the approval output; expects both steps to succeed. + QSB::ApproveProposal_output proposeAndApprove(const id& proposer, const id& approver, const QSB::Propose_input& input) { - QSB::Pause_input input; - QSB::Pause_output output; - - invokeUserProcedure(QSB_CONTRACT_INDEX, 14, input, output, user, 0); - return output; + QSB::Propose_output propOut = propose(proposer, input); + EXPECT_TRUE((bool)propOut.success); + QSB::ApproveProposal_output approveOut = approveProposal(approver, propOut.proposalId); + return approveOut; + } + + // ============================================================================ + // Named proposal helpers + // ============================================================================ + + QSB::ApproveProposal_output proposeAddRole(const id& proposer, const id& approver, uint8 role, const id& account) + { + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropAddRole; + input.role = role; + input.targetId = account; + return proposeAndApprove(proposer, approver, input); } - QSB::Unpause_output unpause(const id& user) + QSB::ApproveProposal_output proposeRemoveRole(const id& proposer, const id& approver, uint8 role, const id& account) { - QSB::Unpause_input input; - QSB::Unpause_output output; - - invokeUserProcedure(QSB_CONTRACT_INDEX, 15, input, output, user, 0); - return output; + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropRemoveRole; + input.role = role; + input.targetId = account; + return proposeAndApprove(proposer, approver, input); } - QSB::EditFeeParameters_output editFeeParameters( - const id& user, - uint32 bpsFee, - uint32 protocolFee, - const id& protocolFeeRecipient, - const id& oracleFeeRecipient) + QSB::ApproveProposal_output proposeEditOracleThreshold(const id& proposer, const id& approver, uint8 newThreshold) { - QSB::EditFeeParameters_input input; - QSB::EditFeeParameters_output output; - + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropEditOracleThreshold; + input.newOracleThreshold = newThreshold; + return proposeAndApprove(proposer, approver, input); + } + + QSB::ApproveProposal_output proposeEditFeeParameters( + const id& proposer, const id& approver, + uint32 bpsFee, uint32 protocolFee, + const id& protocolFeeRecipient, const id& oracleFeeRecipient) + { + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropEditFeeParameters; input.bpsFee = bpsFee; input.protocolFee = protocolFee; input.protocolFeeRecipient = protocolFeeRecipient; input.oracleFeeRecipient = oracleFeeRecipient; - - invokeUserProcedure(QSB_CONTRACT_INDEX, 16, input, output, user, 0); - return output; + return proposeAndApprove(proposer, approver, input); + } + + QSB::ApproveProposal_output proposeUnpause(const id& proposer, const id& approver) + { + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropUnpause; + return proposeAndApprove(proposer, approver, input); + } + + QSB::ApproveProposal_output proposeAddAdmin(const id& proposer, const id& approver, const id& newAdmin) + { + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropAddAdmin; + input.targetId = newAdmin; + return proposeAndApprove(proposer, approver, input); + } + + QSB::ApproveProposal_output proposeRemoveAdmin(const id& proposer, const id& approver, const id& target) + { + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropRemoveAdmin; + input.targetId = target; + return proposeAndApprove(proposer, approver, input); + } + + QSB::ApproveProposal_output proposeSetAdminThreshold(const id& proposer, const id& approver, uint8 newThreshold) + { + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropSetAdminThreshold; + input.newAdminThreshold = newThreshold; + return proposeAndApprove(proposer, approver, input); } // ============================================================================ - // View / helper function wrappers (GetConfig, IsOracle, IsPauser, GetLockedOrder, IsOrderFilled) + // View / helper function wrappers // ============================================================================ void runEndEpoch() @@ -473,6 +541,23 @@ class ContractTestingQSB : protected ContractTesting callFunction(QSB_CONTRACT_INDEX, 10, input, output); return output; } + + QSB::GetProposal_output getProposal(uint8 proposalId) const + { + QSB::GetProposal_input input; + QSB::GetProposal_output output; + input.proposalId = proposalId; + callFunction(QSB_CONTRACT_INDEX, 11, input, output); + return output; + } + + QSB::GetProposals_output getProposals() const + { + QSB::GetProposals_input input; + QSB::GetProposals_output output; + callFunction(QSB_CONTRACT_INDEX, 12, input, output); + return output; + } }; // ============================================================================ @@ -485,7 +570,10 @@ TEST(ContractTestingQSB, TestGetConfig_ReturnsInitialState) QSB::GetConfig_output config = test.getConfig(); - EXPECT_EQ(config.admin, ADMIN); + EXPECT_EQ(config.adminCount, 2); + EXPECT_EQ(config.adminThreshold, 2); + EXPECT_EQ(config.admins.get(0), ADMIN); + EXPECT_EQ(config.admins.get(1), ADMIN2); EXPECT_EQ(config.protocolFeeRecipient, NULL_ID); EXPECT_EQ(config.oracleFeeRecipient, NULL_ID); EXPECT_EQ(config.bpsFee, 0u); @@ -499,12 +587,10 @@ TEST(ContractTestingQSB, TestGetConfig_ReflectsAdminAndFeeChanges) { ContractTestingQSB test; - increaseEnergy(ADMIN, 1); - test.editFeeParameters(ADMIN, 50, 20, PROTOCOL_FEE_RECIPIENT, ORACLE_FEE_RECIPIENT); + test.proposeEditFeeParameters(ADMIN, ADMIN2, 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); @@ -526,9 +612,7 @@ TEST(ContractTestingQSB, TestIsOracle_ReturnsTrueAfterAddRole) { ContractTestingQSB test; - increaseEnergy(ADMIN, 1); - increaseEnergy(ORACLE1, 1); - test.addRole(ADMIN, (uint8)QSB::Role::Oracle, ORACLE1); + test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Oracle, ORACLE1); QSB::IsOracle_output out = test.isOracle(ORACLE1); EXPECT_TRUE((bool)out.isOracle); @@ -541,9 +625,7 @@ TEST(ContractTestingQSB, TestIsPauser_ReturnsFalseWhenNotPauser) { ContractTestingQSB test; - increaseEnergy(ADMIN, 1); - increaseEnergy(PAUSER1, 1); - test.addRole(ADMIN, (uint8)QSB::Role::Pauser, PAUSER1); + test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Pauser, PAUSER1); QSB::IsPauser_output out = test.isPauser(PAUSER1); EXPECT_TRUE((bool)out.isPauser); @@ -556,9 +638,7 @@ TEST(ContractTestingQSB, TestIsPauser_ReturnsTrueAfterAddRole) { ContractTestingQSB test; - increaseEnergy(ADMIN, 1); - increaseEnergy(PAUSER1, 1); - test.addRole(ADMIN, (uint8)QSB::Role::Pauser, PAUSER1); + test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Pauser, PAUSER1); QSB::IsPauser_output out = test.isPauser(PAUSER1); EXPECT_TRUE((bool)out.isPauser); @@ -686,11 +766,8 @@ 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); + test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Oracle, ORACLE1); + test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Oracle, ORACLE2); QSB::GetOracles_output out = test.getOracles(); EXPECT_EQ(out.count, 2u); @@ -710,9 +787,7 @@ TEST(ContractTestingQSB, TestGetPausers_ReturnsAllPausersAfterAddRole) { ContractTestingQSB test; - increaseEnergy(ADMIN, 1); - increaseEnergy(PAUSER1, 1); - test.addRole(ADMIN, (uint8)QSB::Role::Pauser, PAUSER1); + test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Pauser, PAUSER1); QSB::GetPausers_output out = test.getPausers(); EXPECT_EQ(out.count, 1u); @@ -847,6 +922,7 @@ TEST(ContractTestingQSB, TestFilledOrders_RingBufferOverwritesOldEntries) hash.set(1, (uint8)((QSB_MAX_FILLED_ORDERS >> 8) & 0xff)); test.getState()->forceMarkOrderFilled(hash); } + EXPECT_EQ(test.getState()->orderEra, 1u); // Hash 0 is still found — it lives in filledOrdersPrev (grace window) @@ -888,15 +964,18 @@ TEST(ContractTestingQSB, TestFilledOrders_RingBufferOverwritesOldEntries) TEST(ContractTestingQSB, TestInitialization) { ContractTestingQSB test; - + // Check initial state - test.getState()->checkAdmin(ADMIN); + test.getState()->checkAdminCount(2); + test.getState()->checkAdminThreshold(2); + test.getState()->checkIsAdmin(ADMIN); + test.getState()->checkIsAdmin(ADMIN2); 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); } @@ -908,18 +987,18 @@ TEST(ContractTestingQSB, TestInitialization) 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) @@ -936,17 +1015,16 @@ TEST(ContractTestingQSB, TestLock_Success) TEST(ContractTestingQSB, TestLock_FailsWhenPaused) { ContractTestingQSB test; - - increaseEnergy(ADMIN, 1); + increaseEnergy(USER1, 1000000); - - // Pause + + // Pause (single-key, ADMIN is an admin/pauser) 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); @@ -957,10 +1035,10 @@ TEST(ContractTestingQSB, TestLock_FailsWhenPaused) 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); } @@ -1070,17 +1148,17 @@ TEST(ContractTestingQSB, TestLock_FailsWhenNonceAlreadyUsedAndRefunds) 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); @@ -1094,19 +1172,19 @@ TEST(ContractTestingQSB, TestLock_FailsWhenNonceAlreadyUsed) 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); } @@ -1188,94 +1266,386 @@ TEST(ContractTestingQSB, TestOverrideLock_BlockedAfterMaxAttempts) } // ============================================================================ -// Admin Function Tests +// Admin Multisig Tests // ============================================================================ -TEST(ContractTestingQSB, TestTransferAdmin_Success) +TEST(ContractTestingQSB, TestPropose_FailsWhenNotAdmin) { ContractTestingQSB test; - - increaseEnergy(ADMIN, 1); - increaseEnergy(USER1, 1); - QSB::TransferAdmin_output output = test.transferAdmin(ADMIN, USER1); - EXPECT_TRUE(output.success); - - test.getState()->checkAdmin(USER1); + + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropAddRole; + input.role = (uint8)QSB::Role::Oracle; + input.targetId = ORACLE1; + + QSB::Propose_output out = test.propose(USER1, input); + EXPECT_FALSE((bool)out.success); + EXPECT_EQ(out.reasonCode, QSBReasonNotAdmin); } -TEST(ContractTestingQSB, TestTransferAdmin_ToNullId) +TEST(ContractTestingQSB, TestPropose_FailsWhenAdminExceedsConcurrentCap) { ContractTestingQSB test; - increaseEnergy(ADMIN, 1); - QSB::TransferAdmin_output output = test.transferAdmin(ADMIN, NULL_ID); - EXPECT_FALSE(output.success); + // Fill ADMIN's quota (QSB_MAX_PROPOSALS_PER_ADMIN = 3) + QSB::Propose_input input; + for (uint32 i = 0; i < QSB_MAX_PROPOSALS_PER_ADMIN; ++i) + { + setMemory(input, 0); + input.proposalType = QSBPropAddRole; + input.role = (uint8)QSB::Role::Oracle; + input.targetId = ORACLE1; + QSB::Propose_output out = test.propose(ADMIN, input); + EXPECT_TRUE((bool)out.success) << "proposal " << i << " should succeed"; + } - // Admin must not be reset to zero (which would open bootstrap for everyone) - test.getState()->checkAdmin(ADMIN); + // The (cap+1)-th proposal from the same admin must be rejected + setMemory(input, 0); + input.proposalType = QSBPropAddRole; + input.role = (uint8)QSB::Role::Oracle; + input.targetId = ORACLE1; + QSB::Propose_output overflow = test.propose(ADMIN, input); + EXPECT_FALSE((bool)overflow.success); + EXPECT_EQ(overflow.reasonCode, QSBReasonTooManyProposals); + + // Other admins are unaffected — ADMIN2 can still propose + setMemory(input, 0); + input.proposalType = QSBPropAddRole; + input.role = (uint8)QSB::Role::Oracle; + input.targetId = ORACLE1; + QSB::Propose_output admin2Out = test.propose(ADMIN2, input); + EXPECT_TRUE((bool)admin2Out.success); } -TEST(ContractTestingQSB, TestTransferAdmin_FailsWhenNotAdmin) +TEST(ContractTestingQSB, TestPropose_AutoApprovesForProposer) { 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); + + // With threshold=2, one approval doesn't execute yet + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropAddRole; + input.role = (uint8)QSB::Role::Oracle; + input.targetId = ORACLE1; + + QSB::Propose_output propOut = test.propose(ADMIN, input); + EXPECT_TRUE((bool)propOut.success); + + // Proposal exists with approvalCount=1 (proposer auto-approved) + QSB::GetProposal_output propState = test.getProposal(propOut.proposalId); + EXPECT_TRUE((bool)propState.exists); + EXPECT_EQ(propState.proposal.approvalCount, 1); + EXPECT_EQ(propState.proposal.executed, 0); + + // Role not yet added + QSB::IsOracle_output oracleOut = test.isOracle(ORACLE1); + EXPECT_FALSE((bool)oracleOut.isOracle); +} + +TEST(ContractTestingQSB, TestApproveProposal_ExecutesAtThreshold) +{ + ContractTestingQSB test; + + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropAddRole; + input.role = (uint8)QSB::Role::Oracle; + input.targetId = ORACLE1; + + QSB::Propose_output propOut = test.propose(ADMIN, input); + EXPECT_TRUE((bool)propOut.success); + + // Second admin approves → threshold=2 reached → executes + QSB::ApproveProposal_output approveOut = test.approveProposal(ADMIN2, propOut.proposalId); + EXPECT_TRUE((bool)approveOut.success); + EXPECT_TRUE((bool)approveOut.executed); + + // Role was added + QSB::IsOracle_output oracleOut = test.isOracle(ORACLE1); + EXPECT_TRUE((bool)oracleOut.isOracle); +} + +TEST(ContractTestingQSB, TestApproveProposal_FailsWhenNotAdmin) +{ + ContractTestingQSB test; + + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropAddRole; + input.role = (uint8)QSB::Role::Oracle; + input.targetId = ORACLE1; + + QSB::Propose_output propOut = test.propose(ADMIN, input); + EXPECT_TRUE((bool)propOut.success); + + QSB::ApproveProposal_output approveOut = test.approveProposal(USER1, propOut.proposalId); + EXPECT_FALSE((bool)approveOut.success); + EXPECT_EQ(approveOut.reasonCode, QSBReasonNotAdmin); } +TEST(ContractTestingQSB, TestApproveProposal_FailsOnDuplicateApproval) +{ + ContractTestingQSB test; + + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropAddRole; + input.role = (uint8)QSB::Role::Oracle; + input.targetId = ORACLE1; + + QSB::Propose_output propOut = test.propose(ADMIN, input); + EXPECT_TRUE((bool)propOut.success); + + // ADMIN already auto-approved when proposing; approving again should fail + QSB::ApproveProposal_output approveOut = test.approveProposal(ADMIN, propOut.proposalId); + EXPECT_FALSE((bool)approveOut.success); + EXPECT_EQ(approveOut.reasonCode, QSBReasonAlreadyApproved); +} + +TEST(ContractTestingQSB, TestCancelProposal_ByProposer) +{ + ContractTestingQSB test; + + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropAddRole; + input.role = (uint8)QSB::Role::Oracle; + input.targetId = ORACLE1; + + QSB::Propose_output propOut = test.propose(ADMIN, input); + EXPECT_TRUE((bool)propOut.success); + + QSB::CancelProposal_output cancelOut = test.cancelProposal(ADMIN, propOut.proposalId); + EXPECT_TRUE((bool)cancelOut.success); + + // Proposal no longer active + QSB::GetProposal_output propState = test.getProposal(propOut.proposalId); + EXPECT_FALSE((bool)propState.exists); +} + +TEST(ContractTestingQSB, TestCancelProposal_FailsWhenNotProposer) +{ + ContractTestingQSB test; + + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropAddRole; + input.role = (uint8)QSB::Role::Oracle; + input.targetId = ORACLE1; + + QSB::Propose_output propOut = test.propose(ADMIN, input); + EXPECT_TRUE((bool)propOut.success); + + // ADMIN2 is not the proposer + QSB::CancelProposal_output cancelOut = test.cancelProposal(ADMIN2, propOut.proposalId); + EXPECT_FALSE((bool)cancelOut.success); + EXPECT_EQ(cancelOut.reasonCode, QSBReasonNotProposer); +} + +TEST(ContractTestingQSB, TestAddAdmin_Success) +{ + ContractTestingQSB test; + + // Add a third admin + QSB::ApproveProposal_output out = test.proposeAddAdmin(ADMIN, ADMIN2, USER1); + EXPECT_TRUE((bool)out.success); + EXPECT_TRUE((bool)out.executed); + + test.getState()->checkAdminCount(3); + test.getState()->checkIsAdmin(USER1); +} + +TEST(ContractTestingQSB, TestAddAdmin_FailsWhenAlreadyAdmin) +{ + ContractTestingQSB test; + + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropAddAdmin; + input.targetId = ADMIN2; // already an admin + + // Proposal creation should fail (or execution should fail) + QSB::Propose_output propOut = test.propose(ADMIN, input); + if ((bool)propOut.success) + { + QSB::ApproveProposal_output approveOut = test.approveProposal(ADMIN2, propOut.proposalId); + // Execution must reject AlreadyAdmin + EXPECT_FALSE((bool)approveOut.executed); + } + // Either way admin count stays 2 + test.getState()->checkAdminCount(2); +} + +TEST(ContractTestingQSB, TestRemoveAdmin_Success) +{ + ContractTestingQSB test; + + // Start: 2 admins, threshold=2. Lower threshold first so we don't lock. + test.proposeSetAdminThreshold(ADMIN, ADMIN2, 1); + test.getState()->checkAdminThreshold(1); + + // Now remove ADMIN2 + test.proposeRemoveAdmin(ADMIN, ADMIN2, ADMIN2); + test.getState()->checkAdminCount(1); + test.getState()->checkIsNotAdmin(ADMIN2); +} + +TEST(ContractTestingQSB, TestRemoveAdmin_FailsWouldLockContract) +{ + ContractTestingQSB test; + + // With threshold=2 and adminCount=2, removing one admin would leave 1 < threshold. + // The Propose procedure itself rejects this before creating a proposal. + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropRemoveAdmin; + input.targetId = ADMIN2; + + QSB::Propose_output propOut = test.propose(ADMIN, input); + EXPECT_FALSE((bool)propOut.success); + EXPECT_EQ(propOut.reasonCode, QSBReasonWouldLockContract); + + test.getState()->checkAdminCount(2); +} + +TEST(ContractTestingQSB, TestSetAdminThreshold_Success) +{ + ContractTestingQSB test; + + // First add a third admin so threshold can be set to 3 + test.proposeAddAdmin(ADMIN, ADMIN2, USER1); + test.getState()->checkAdminCount(3); + + test.proposeSetAdminThreshold(ADMIN, ADMIN2, 3); + test.getState()->checkAdminThreshold(3); +} + +TEST(ContractTestingQSB, TestSetAdminThreshold_FailsWouldLockContract) +{ + ContractTestingQSB test; + + // threshold=2 and adminCount=2: trying to set threshold=3 would exceed adminCount. + // The Propose procedure itself rejects this before creating a proposal. + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropSetAdminThreshold; + input.newAdminThreshold = 3; + + QSB::Propose_output propOut = test.propose(ADMIN, input); + EXPECT_FALSE((bool)propOut.success); + EXPECT_EQ(propOut.reasonCode, QSBReasonInvalidThreshold); + + test.getState()->checkAdminThreshold(2); +} + +TEST(ContractTestingQSB, TestAdminSetChange_CancelsAllPendingProposals) +{ + ContractTestingQSB test; + + // Create a pending proposal + QSB::Propose_input pendingInput; + setMemory(pendingInput, 0); + pendingInput.proposalType = QSBPropAddRole; + pendingInput.role = (uint8)QSB::Role::Oracle; + pendingInput.targetId = ORACLE1; + + QSB::Propose_output pendingProp = test.propose(ADMIN, pendingInput); + EXPECT_TRUE((bool)pendingProp.success); + + // Confirm it's active + QSB::GetProposal_output before = test.getProposal(pendingProp.proposalId); + EXPECT_TRUE((bool)before.exists); + + // Execute an AddAdmin proposal (admin set change) + test.proposeAddAdmin(ADMIN, ADMIN2, USER2); + + // The pending AddRole proposal should have been cancelled + QSB::GetProposal_output after = test.getProposal(pendingProp.proposalId); + EXPECT_FALSE((bool)after.exists); +} + +TEST(ContractTestingQSB, TestProposalExpiry_ExpiredProposalIsInactive) +{ + ContractTestingQSB test; + + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropAddRole; + input.role = (uint8)QSB::Role::Oracle; + input.targetId = ORACLE1; + + QSB::Propose_output propOut = test.propose(ADMIN, input); + EXPECT_TRUE((bool)propOut.success); + + // Advance system.epoch past QSB_PROPOSAL_EXPIRY_EPOCHS and trigger END_EPOCH sweep. + // (qpi.epoch() reads system.epoch which is 0 by default in tests) + system.epoch = QSB_PROPOSAL_EXPIRY_EPOCHS + 1; + test.runEndEpoch(); + + // Proposal should be expired (inactive) + QSB::GetProposal_output propState = test.getProposal(propOut.proposalId); + EXPECT_FALSE((bool)propState.exists); + + // Trying to approve the expired proposal should fail + QSB::ApproveProposal_output approveOut = test.approveProposal(ADMIN2, propOut.proposalId); + EXPECT_FALSE((bool)approveOut.success); + + // Restore epoch for subsequent tests + system.epoch = 0; +} + +// ============================================================================ +// Admin Function Tests (proposal-based) +// ============================================================================ + 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 - + + QSB::ApproveProposal_output out = test.proposeEditOracleThreshold(ADMIN, ADMIN2, 75); + EXPECT_TRUE((bool)out.success); + EXPECT_TRUE((bool)out.executed); + 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); - + + QSB::ApproveProposal_output out = test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Oracle, ORACLE1); + EXPECT_TRUE((bool)out.success); + EXPECT_TRUE((bool)out.executed); + 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); + + QSB::ApproveProposal_output out = test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Pauser, PAUSER1); + EXPECT_TRUE((bool)out.success); + EXPECT_TRUE((bool)out.executed); } TEST(ContractTestingQSB, TestAddRole_InvalidRole) { ContractTestingQSB test; - increaseEnergy(ADMIN, 1); - QSB::AddRole_output output = test.addRole(ADMIN, 99, USER1); - EXPECT_FALSE(output.success); + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropAddRole; + input.role = 99; // invalid + input.targetId = USER1; + + QSB::Propose_output propOut = test.propose(ADMIN, input); + if ((bool)propOut.success) + { + QSB::ApproveProposal_output approveOut = test.approveProposal(ADMIN2, propOut.proposalId); + EXPECT_FALSE((bool)approveOut.executed); + } test.getState()->checkOracleCount(0); } @@ -1283,51 +1653,72 @@ TEST(ContractTestingQSB, TestAddRole_InvalidRole) TEST(ContractTestingQSB, TestAddRole_OracleFull) { ContractTestingQSB test; - increaseEnergy(ADMIN, 1); for (uint32_t i = 0; i < QSB_MAX_ORACLES; ++i) { id oracle(i + 1, 0, 0, 0); - QSB::AddRole_output out = test.addRole(ADMIN, (uint8)QSB::Role::Oracle, oracle); - EXPECT_TRUE(out.success); + QSB::ApproveProposal_output out = test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Oracle, oracle); + EXPECT_TRUE((bool)out.executed); } test.getState()->checkOracleCount(QSB_MAX_ORACLES); id extra(QSB_MAX_ORACLES + 1, 0, 0, 0); - QSB::AddRole_output output = test.addRole(ADMIN, (uint8)QSB::Role::Oracle, extra); - EXPECT_FALSE(output.success); + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropAddRole; + input.role = (uint8)QSB::Role::Oracle; + input.targetId = extra; + QSB::Propose_output propOut = test.propose(ADMIN, input); + if ((bool)propOut.success) + { + // executed=true means threshold was reached, not that the payload succeeded + test.approveProposal(ADMIN2, propOut.proposalId); + } + // Extra oracle must not have been added + QSB::IsOracle_output isOracleOut = test.isOracle(extra); + EXPECT_FALSE((bool)isOracleOut.isOracle); test.getState()->checkOracleCount(QSB_MAX_ORACLES); } TEST(ContractTestingQSB, TestAddRole_PauserFull) { ContractTestingQSB test; - increaseEnergy(ADMIN, 1); for (uint32_t i = 0; i < QSB_MAX_PAUSERS; ++i) { id pauser(i + 1, 0, 0, 0); - QSB::AddRole_output out = test.addRole(ADMIN, (uint8)QSB::Role::Pauser, pauser); - EXPECT_TRUE(out.success); + QSB::ApproveProposal_output out = test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Pauser, pauser); + EXPECT_TRUE((bool)out.executed); } id extra(QSB_MAX_PAUSERS + 1, 0, 0, 0); - QSB::AddRole_output output = test.addRole(ADMIN, (uint8)QSB::Role::Pauser, extra); - EXPECT_FALSE(output.success); + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropAddRole; + input.role = (uint8)QSB::Role::Pauser; + input.targetId = extra; + QSB::Propose_output propOut = test.propose(ADMIN, input); + if ((bool)propOut.success) + { + // executed=true means threshold was reached, not that the payload succeeded + test.approveProposal(ADMIN2, propOut.proposalId); + } + + // Extra pauser must not have been added + QSB::IsPauser_output isPauserOut = test.isPauser(extra); + EXPECT_FALSE((bool)isPauserOut.isPauser); } TEST(ContractTestingQSB, TestRemoveRole_Oracle) { ContractTestingQSB test; - // Bootstrap admin and add oracle - increaseEnergy(ADMIN, 1); - test.addRole(ADMIN, (uint8)QSB::Role::Oracle, ORACLE1); + test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Oracle, ORACLE1); - // Now remove it - QSB::RemoveRole_output output = test.removeRole(ADMIN, (uint8)QSB::Role::Oracle, ORACLE1); - EXPECT_TRUE(output.success); + QSB::ApproveProposal_output out = test.proposeRemoveRole(ADMIN, ADMIN2, (uint8)QSB::Role::Oracle, ORACLE1); + EXPECT_TRUE((bool)out.success); + EXPECT_TRUE((bool)out.executed); test.getState()->checkOracleCount(0); } @@ -1335,61 +1726,56 @@ TEST(ContractTestingQSB, TestRemoveRole_Oracle) 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 + + test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Pauser, PAUSER1); + + // Pauser can pause without going through a proposal QSB::Pause_output output = test.pause(PAUSER1); EXPECT_TRUE(output.success); - + test.getState()->checkPaused(true); } -TEST(ContractTestingQSB, TestUnpause_ByAdmin) +TEST(ContractTestingQSB, TestUnpause_ByAdminProposal) { ContractTestingQSB test; - increaseEnergy(ADMIN, 1); test.pause(ADMIN); + test.getState()->checkPaused(true); - QSB::Unpause_output output = test.unpause(ADMIN); - EXPECT_TRUE(output.success); + QSB::ApproveProposal_output out = test.proposeUnpause(ADMIN, ADMIN2); + EXPECT_TRUE((bool)out.success); + EXPECT_TRUE((bool)out.executed); test.getState()->checkPaused(false); } -TEST(ContractTestingQSB, TestUnpause_FailsForPauser) +TEST(ContractTestingQSB, TestUnpause_FailsForNonAdmin) { ContractTestingQSB test; - increaseEnergy(ADMIN, 1); - increaseEnergy(PAUSER1, 1); - - test.addRole(ADMIN, (uint8)QSB::Role::Pauser, PAUSER1); + test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Pauser, PAUSER1); test.pause(PAUSER1); test.getState()->checkPaused(true); - // Pauser must not be able to cancel their own pause - QSB::Unpause_output output = test.unpause(PAUSER1); - EXPECT_FALSE(output.success); + // Pauser must not be able to unpause (it's admin-only via proposal) + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropUnpause; + + QSB::Propose_output propOut = test.propose(PAUSER1, input); + EXPECT_FALSE((bool)propOut.success); + EXPECT_EQ(propOut.reasonCode, QSBReasonNotAdmin); test.getState()->checkPaused(true); } @@ -1397,13 +1783,11 @@ TEST(ContractTestingQSB, TestUnpause_FailsForPauser) 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); - + + QSB::ApproveProposal_output out = test.proposeEditFeeParameters(ADMIN, ADMIN2, 100, 30, PROTOCOL_FEE_RECIPIENT, ORACLE_FEE_RECIPIENT); + EXPECT_TRUE((bool)out.success); + EXPECT_TRUE((bool)out.executed); + test.getState()->checkBpsFee(100); test.getState()->checkProtocolFee(30); test.getState()->checkProtocolFeeRecipient(PROTOCOL_FEE_RECIPIENT); @@ -1414,12 +1798,17 @@ TEST(ContractTestingQSB, TestEditFeeParameters_RejectsTooHighBpsFee) { ContractTestingQSB test; - // Bootstrap admin - increaseEnergy(ADMIN, 1); + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropEditFeeParameters; + input.bpsFee = QSB_MAX_BPS_FEE + 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); + QSB::Propose_output propOut = test.propose(ADMIN, input); + if ((bool)propOut.success) + { + QSB::ApproveProposal_output out = test.approveProposal(ADMIN2, propOut.proposalId); + EXPECT_FALSE((bool)out.executed); + } // State should remain unchanged test.getState()->checkBpsFee(0); @@ -1429,13 +1818,22 @@ 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); + // Set an initial valid configuration first + test.proposeEditFeeParameters(ADMIN, ADMIN2, 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); + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropEditFeeParameters; + input.bpsFee = 0; + input.protocolFee = QSB_MAX_PROTOCOL_FEE + 1; + + QSB::Propose_output propOut = test.propose(ADMIN, input); + if ((bool)propOut.success) + { + QSB::ApproveProposal_output out = test.approveProposal(ADMIN2, propOut.proposalId); + EXPECT_FALSE((bool)out.executed); + } // State should still reflect the previous valid configuration test.getState()->checkProtocolFee(10); @@ -1470,17 +1868,15 @@ TEST(ContractTestingQSB, TestUnlock_FailsWhenNoOracles) TEST(ContractTestingQSB, TestUnlock_FailsWhenPaused) { ContractTestingQSB test; - - // Bootstrap admin, add oracle, and pause - increaseEnergy(ADMIN, 1); - test.addRole(ADMIN, (uint8)QSB::Role::Oracle, ORACLE1); + + test.proposeAddRole(ADMIN, ADMIN2, (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 } @@ -1557,21 +1953,21 @@ TEST(ContractTestingQSB, TestUnlock_DoesNotRequireMatchingLock) 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) @@ -1588,27 +1984,21 @@ TEST(ContractTestingQSB, TestFullWorkflow_LockAndOverride) 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); - + + // Add oracles via multisig proposals + test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Oracle, ORACLE1); + test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Oracle, ORACLE2); + test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Oracle, ORACLE3); + test.getState()->checkOracleCount(3); - - // Step 3: Set threshold - test.editOracleThreshold(ADMIN, 67); // 2/3 + 1 + + // Update oracle threshold + test.proposeEditOracleThreshold(ADMIN, ADMIN2, 67); // 2/3 test.getState()->checkOracleThreshold(67); - - // Step 4: Configure fees - test.editFeeParameters(ADMIN, 50, 20, PROTOCOL_FEE_RECIPIENT, ORACLE_FEE_RECIPIENT); - + + // Configure fees + test.proposeEditFeeParameters(ADMIN, ADMIN2, 50, 20, PROTOCOL_FEE_RECIPIENT, ORACLE_FEE_RECIPIENT); + test.getState()->checkBpsFee(50); test.getState()->checkProtocolFee(20); } @@ -1627,7 +2017,6 @@ TEST(ContractTestingQSB, TestGetConfig_ReturnsOrderEra) TEST(ContractTestingQSB, TestLock_StoresCurrentEra) { ContractTestingQSB test; - increaseEnergy(ADMIN, 1); uint64 amount = 10000; uint32 nonce = 1; @@ -1686,7 +2075,6 @@ TEST(ContractTestingQSB, TestFilledOrders_EraIncrementsOnWrap) TEST(ContractTestingQSB, TestOverrideLock_PreservesOriginalEra) { ContractTestingQSB test; - increaseEnergy(ADMIN, 1); uint64 amount = 10000; uint32 nonce = 42; @@ -1721,12 +2109,10 @@ TEST(ContractTestingQSB, TestOverrideLock_PreservesOriginalEra) 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); + // Setup: add oracle, set threshold to 1 via proposals + test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Oracle, ORACLE1); + test.proposeEditOracleThreshold(ADMIN, ADMIN2, 1); // Fund contract with some balance uint64 amount = 10000; @@ -1743,172 +2129,3 @@ TEST(ContractTestingQSB, TestUnlock_FailsWhenEraMismatch) 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, grace window only covers era=2) - 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 -} - -// Helper: fill the ring buffer once to advance era by 1 -static void advanceEra(ContractTestingQSB& test, uint32 era) -{ - for (uint32 round = 0; round < era; ++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); - } - } -} - -// Unlock with era N-1 succeeds immediately after a ring-buffer wrap (grace window). -TEST(ContractTestingQSB, TestUnlock_PreviousEraAccepted) -{ - ContractTestingQSB test; - increaseEnergy(ADMIN, 1); - auto oracle = ContractTestingQSB::makeOracleKey(1001); - increaseEnergy(oracle.publicKey, 1); - test.addRole(ADMIN, (uint8)QSB::Role::Oracle, oracle.publicKey); - test.editOracleThreshold(ADMIN, 1); - - uint64 amount = 10000; - increaseEnergy(USER1, amount); - test.lock(USER1, amount, 0, 1, 1, ContractTestingQSB::createZeroAddress(), amount); - - // Advance to era 1 - advanceEra(test, 1); - EXPECT_EQ(test.getState()->orderEra, 1u); - - // Order signed with era=0 (previous era) should still succeed - QSB::Order order = ContractTestingQSB::createTestOrderFromU32Nonce(USER1, USER2, amount, 10, 42, 0); - Array sigs; - setMemory(sigs, 0); - sigs.set(0, test.createOrderSignature(oracle, order)); - QSB::Unlock_output result = test.unlock(USER1, order, 1, sigs); - EXPECT_TRUE(result.success); -} - -// Unlock with era N-2 is rejected even with the grace window. -TEST(ContractTestingQSB, TestUnlock_TwoErasAgoRejected) -{ - ContractTestingQSB test; - increaseEnergy(ADMIN, 1); - auto oracle = ContractTestingQSB::makeOracleKey(1002); - increaseEnergy(oracle.publicKey, 1); - test.addRole(ADMIN, (uint8)QSB::Role::Oracle, oracle.publicKey); - test.editOracleThreshold(ADMIN, 1); - - uint64 amount = 10000; - increaseEnergy(USER1, amount); - test.lock(USER1, amount, 0, 1, 1, ContractTestingQSB::createZeroAddress(), amount); - - // Advance to era 2 - advanceEra(test, 2); - EXPECT_EQ(test.getState()->orderEra, 2u); - - // Order signed with era=0 (two eras ago) is rejected (era check fails before sig check) - QSB::Order order = ContractTestingQSB::createTestOrderFromU32Nonce(USER1, USER2, amount, 10, 42, 0); - Array sigs; - setMemory(sigs, 0); - sigs.set(0, test.createOrderSignature(oracle, order)); - QSB::Unlock_output result = test.unlock(USER1, order, 1, sigs); - EXPECT_FALSE(result.success); -} - -// An order filled in era N-1 cannot be replayed in era N using the grace window. -TEST(ContractTestingQSB, TestUnlock_NoReplayAfterEraTransition) -{ - ContractTestingQSB test; - increaseEnergy(ADMIN, 1); - auto oracle = ContractTestingQSB::makeOracleKey(1003); - increaseEnergy(oracle.publicKey, 1); - test.addRole(ADMIN, (uint8)QSB::Role::Oracle, oracle.publicKey); - test.editOracleThreshold(ADMIN, 1); - - uint64 amount = 20000; - increaseEnergy(USER1, amount * 2); - test.lock(USER1, amount * 2, 0, 1, 1, ContractTestingQSB::createZeroAddress(), amount * 2); - - // Fill the order in era 0 - QSB::Order order = ContractTestingQSB::createTestOrderFromU32Nonce(USER1, USER2, amount, 10, 77, 0); - Array sigs; - setMemory(sigs, 0); - sigs.set(0, test.createOrderSignature(oracle, order)); - QSB::Unlock_output first = test.unlock(USER1, order, 1, sigs); - EXPECT_TRUE(first.success); - - // Advance to era 1 - advanceEra(test, 1); - EXPECT_EQ(test.getState()->orderEra, 1u); - - // Replay attempt with same order (era=0 accepted by grace window) must fail — isOrderFilled blocks it - QSB::Unlock_output replay = test.unlock(USER1, order, 1, sigs); - EXPECT_FALSE(replay.success); -} - -TEST(ContractTestingQSB, PrintStructSizes) { -#define PRINT_QSB(fn) printf("%-22s in=%3zu out=%3zu loc=%3zu total=%4zu rem=%zu\n", \ - #fn, sizeof(QSB::fn##_input), sizeof(QSB::fn##_output), sizeof(QSB::fn##_locals), \ - sizeof(QSB::fn##_input)+sizeof(QSB::fn##_output)+sizeof(QSB::fn##_locals), \ - (sizeof(QSB::fn##_input)+sizeof(QSB::fn##_output)+sizeof(QSB::fn##_locals))%4) - PRINT_QSB(Lock); - PRINT_QSB(OverrideLock); - PRINT_QSB(Unlock); - PRINT_QSB(TransferAdmin); - PRINT_QSB(EditOracleThreshold); - PRINT_QSB(AddRole); - PRINT_QSB(RemoveRole); - PRINT_QSB(Pause); - PRINT_QSB(Unpause); - PRINT_QSB(EditFeeParameters); - PRINT_QSB(GetConfig); - PRINT_QSB(IsOracle); - PRINT_QSB(IsPauser); - PRINT_QSB(GetLockedOrder); - PRINT_QSB(IsOrderFilled); - PRINT_QSB(ComputeOrderHash); - PRINT_QSB(GetOracles); - PRINT_QSB(GetPausers); - PRINT_QSB(GetLockedOrders); - PRINT_QSB(GetFilledOrders); -#undef PRINT_QSB -} From 741461df713eaf079aaf74c8e4697a053ca0c016 Mon Sep 17 00:00:00 2001 From: Jean Date: Wed, 10 Jun 2026 10:58:23 +0200 Subject: [PATCH 24/28] refactor(QSB): extract try* helpers, remove dummy params, single log point per procedure All procedure logic now lives in inline static tryXxx helpers that return a typed result struct. The PUBLIC_PROCEDURE_WITH_LOCALS shell calls the helper, maps outputs, builds the log message, and calls LOG_INFO once. - Phase 1: removed dummy trailing params (i, j, same, entry) from all 12 helpers; variables are now regular C++ locals inside each function - Phase 2: added 7 result structs (LockResult, OverrideLockResult, UnlockResult, ProposeResult, ApproveProposalResult, CancelProposalResult, PauseResult) - Phase 3+4: extracted tryLock, tryOverrideLock, tryUnlock, tryPropose, tryApproveProposal, tryCancelProposal, tryPause; procedure shells reduced to ~12-16 lines each - Phase 5: extracted sweepExpiredProposals from END_EPOCH - Removed trivial comments; kept non-obvious constraints and design notes - 80/80 tests passing Co-Authored-By: Claude Sonnet 4.6 --- src/contracts/QubicSolanaBridge.h | 2123 +++++++++++++++-------------- test/contract_qsb.cpp | 4 +- 2 files changed, 1115 insertions(+), 1012 deletions(-) diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h index 69078702..ea530fb7 100644 --- a/src/contracts/QubicSolanaBridge.h +++ b/src/contracts/QubicSolanaBridge.h @@ -8,8 +8,8 @@ static constexpr uint32 QSB_MAX_ORACLES = 64; static constexpr uint32 QSB_MAX_PAUSERS = 32; static constexpr uint32 QSB_MAX_FILLED_ORDERS = 256; // QPI::Array requires power-of-2; 256 = 5x the ~50 max concurrent orders 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 +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 static constexpr uint8 QSB_OVERRIDE_LOCK_MAX_ATTEMPTS = 3; static constexpr uint32 QSB_MAX_UNLOCK_SIGNATURES = 8; // max sigs per Unlock call; keeps Unlock_input ≤ 1024 bytes @@ -17,21 +17,21 @@ static constexpr uint32 QSB_MAX_UNLOCK_SIGNATURES = 8; // max sigs per Unlock ca // Layout: 245 bytes total. protocolName is padded to 16 (next power of 2 above 11). struct QSBOrderMessage { - uint32 protocolNameLen; // 0: always 11 - Array protocolName; // 4: QubicBridge (11 used, 5 zero-padded) - uint32 protocolVersionLen; // 20: always 1 - Array protocolVersion; // 24: version byte (49 = ASCII '1') - Array contractAddress; // 25: destination contract address (LE-padded index) - uint32 networkIn; // 57 - uint32 networkOut; // 61 - Array tokenIn; // 65 - Array tokenOut; // 97 - Array fromAddress; // 129 - Array toAddress; // 161 - uint64 amount; // 193 - uint64 relayerFee; // 201 - Array nonce; // 209 - uint32 orderEra; // 241 + uint32 protocolNameLen; // 0: always 11 + Array protocolName; // 4: QubicBridge (11 used, 5 zero-padded) + uint32 protocolVersionLen; // 20: always 1 + Array protocolVersion; // 24: version byte (49 = ASCII '1') + Array contractAddress; // 25: destination contract address (LE-padded index) + uint32 networkIn; // 57 + uint32 networkOut; // 61 + Array tokenIn; // 65 + Array tokenOut; // 97 + Array fromAddress; // 129 + Array toAddress; // 161 + uint64 amount; // 193 + uint64 relayerFee; // 201 + Array nonce; // 209 + uint32 orderEra; // 241 }; static constexpr uint32 QSB_QUERY_MAX_PAGE_SIZE = 64; // max entries per paginated query @@ -46,26 +46,26 @@ static constexpr uint32 QSBLogThresholdUpdated = 7; static constexpr uint32 QSBLogRoleGranted = 8; static constexpr uint32 QSBLogRoleRevoked = 9; static constexpr uint32 QSBLogFeeParametersUpdated = 10; -static constexpr uint32 QSBLogProposalCreated = 11; -static constexpr uint32 QSBLogProposalApproved = 12; -static constexpr uint32 QSBLogProposalExecuted = 13; +static constexpr uint32 QSBLogProposalCreated = 11; +static constexpr uint32 QSBLogProposalApproved = 12; +static constexpr uint32 QSBLogProposalExecuted = 13; static constexpr uint32 QSBLogProposalCancelled = 14; // Multisig admin constants -static constexpr uint32 QSB_MAX_ADMINS = 8; // approvedMask is uint8; must stay ≤ 8 -static constexpr uint32 QSB_MAX_PROPOSALS = 16; -static constexpr uint32 QSB_MAX_PROPOSALS_PER_ADMIN = 3; -static constexpr uint32 QSB_PROPOSAL_EXPIRY_EPOCHS = 4; // ~4 weeks +static constexpr uint32 QSB_MAX_ADMINS = 8; // approvedMask is uint8; must stay ≤ 8 +static constexpr uint32 QSB_MAX_PROPOSALS = 16; +static constexpr uint32 QSB_MAX_PROPOSALS_PER_ADMIN = 3; +static constexpr uint32 QSB_PROPOSAL_EXPIRY_EPOCHS = 4; // ~4 weeks // Proposal types -static constexpr uint8 QSBPropAddAdmin = 1; -static constexpr uint8 QSBPropRemoveAdmin = 2; -static constexpr uint8 QSBPropSetAdminThreshold = 3; -static constexpr uint8 QSBPropAddRole = 4; -static constexpr uint8 QSBPropRemoveRole = 5; +static constexpr uint8 QSBPropAddAdmin = 1; +static constexpr uint8 QSBPropRemoveAdmin = 2; +static constexpr uint8 QSBPropSetAdminThreshold = 3; +static constexpr uint8 QSBPropAddRole = 4; +static constexpr uint8 QSBPropRemoveRole = 5; static constexpr uint8 QSBPropEditOracleThreshold = 6; -static constexpr uint8 QSBPropEditFeeParameters = 7; -static constexpr uint8 QSBPropUnpause = 8; +static constexpr uint8 QSBPropEditFeeParameters = 7; +static constexpr uint8 QSBPropUnpause = 8; // Generic reason codes for logging static constexpr uint8 QSBReasonNone = 0; @@ -94,15 +94,15 @@ static constexpr uint8 QSBReasonInvalidRole = 22; static constexpr uint8 QSBReasonOrderNotFound = 23; static constexpr uint8 QSBReasonOverrideLimitReached = 24; // Multisig admin reason codes -static constexpr uint8 QSBReasonProposalNotFound = 25; -static constexpr uint8 QSBReasonProposalExpired = 26; -static constexpr uint8 QSBReasonAlreadyApproved = 27; -static constexpr uint8 QSBReasonProposalFull = 28; -static constexpr uint8 QSBReasonWouldLockContract = 29; -static constexpr uint8 QSBReasonNotProposer = 30; -static constexpr uint8 QSBReasonAlreadyAdmin = 31; -static constexpr uint8 QSBReasonAdminFull = 32; -static constexpr uint8 QSBReasonTooManyProposals = 33; +static constexpr uint8 QSBReasonProposalNotFound = 25; +static constexpr uint8 QSBReasonProposalExpired = 26; +static constexpr uint8 QSBReasonAlreadyApproved = 27; +static constexpr uint8 QSBReasonProposalFull = 28; +static constexpr uint8 QSBReasonWouldLockContract = 29; +static constexpr uint8 QSBReasonNotProposer = 30; +static constexpr uint8 QSBReasonAlreadyAdmin = 31; +static constexpr uint8 QSBReasonAdminFull = 32; +static constexpr uint8 QSBReasonTooManyProposals = 33; static constexpr uint8 QSBReasonInvalidProposalType = 34; struct QSB2 @@ -143,8 +143,8 @@ struct QSB : public ContractBase // Signature wrapper compatible with QPI::signatureValidity struct SignatureData { - id signer; // oracle id (public key) - Array signature; // raw 64-byte signature + id signer; // oracle id (public key) + Array signature; // raw 64-byte signature }; // Storage entry for filledOrders mapping @@ -174,7 +174,7 @@ struct QSB : public ContractBase uint32 lockEpoch; uint32 orderEra; bit active; - uint8 overrideLockCount; // at +161; 6 bytes padding follow to keep struct at 168 bytes + uint8 overrideLockCount; // at +161; 6 bytes padding follow to keep struct at 168 bytes }; // Logging messages @@ -288,36 +288,36 @@ struct QSB : public ContractBase { uint32 _contractIndex; uint32 _type; - uint8 proposalId; - uint8 proposalType; - id proposer; - id actor; - uint8 approvalCount; - uint8 success; - uint8 reasonCode; - sint8 _terminator; + uint8 proposalId; + uint8 proposalType; + id proposer; + id actor; + uint8 approvalCount; + uint8 success; + uint8 reasonCode; + sint8 _terminator; }; // Union-style: fields used depend on proposalType. Unused fields are zero. struct AdminProposal { - uint8 proposalType; // QSBProp* constant - uint8 active; // 1 = slot in use - uint8 executed; // 1 = executed successfully + uint8 proposalType; // QSBProp* constant + uint8 active; // 1 = slot in use + uint8 executed; // 1 = executed successfully - id proposer; // admin who created this proposal - uint32 createdEpoch; // for expiry: createdEpoch + QSB_PROPOSAL_EXPIRY_EPOCHS + id proposer; // admin who created this proposal + uint32 createdEpoch; // for expiry: createdEpoch + QSB_PROPOSAL_EXPIRY_EPOCHS - uint8 approvalCount; // cached popcount of approvedMask - uint8 approvedMask; // bit i = admins[i] approved (max 8 admins) + uint8 approvalCount; // cached popcount of approvedMask + uint8 approvedMask; // bit i = admins[i] approved (max 8 admins) // Payload — fields used depend on proposalType - id targetId; // AddAdmin, RemoveAdmin, AddRole/RemoveRole account - uint8 role; // AddRole, RemoveRole: (uint8)Role::Oracle or Role::Pauser - uint8 newAdminThreshold; // SetAdminThreshold - uint8 newOracleThreshold; // EditOracleThreshold - id protocolFeeRecipient; - id oracleFeeRecipient; + id targetId; // AddAdmin, RemoveAdmin, AddRole/RemoveRole account + uint8 role; // AddRole, RemoveRole: (uint8)Role::Oracle or Role::Pauser + uint8 newAdminThreshold; // SetAdminThreshold + uint8 newOracleThreshold; // EditOracleThreshold + id protocolFeeRecipient; + id oracleFeeRecipient; uint32 bpsFee; uint32 protocolFee; }; @@ -329,7 +329,6 @@ struct QSB : public ContractBase // 1) lock() struct Lock_input { - // Recipient on Solana (fixed-size buffer, zero-padded) uint64 amount; uint64 relayerFee; Array toAddress; @@ -398,7 +397,7 @@ struct QSB : public ContractBase struct AddRole_input { id account; - uint8 role; // see Role enum + uint8 role; // see Role enum }; struct AddRole_output @@ -428,16 +427,16 @@ struct QSB : public ContractBase bit success; }; - typedef Pause_input Unpause_input; + 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) + 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 @@ -448,33 +447,57 @@ struct QSB : public ContractBase // Propose: create a typed admin proposal (proposer auto-approves) struct Propose_input { - uint8 proposalType; - id targetId; - uint8 role; - uint8 newAdminThreshold; - uint8 newOracleThreshold; - id protocolFeeRecipient; - id oracleFeeRecipient; + uint8 proposalType; + id targetId; + uint8 role; + uint8 newAdminThreshold; + uint8 newOracleThreshold; + id protocolFeeRecipient; + id oracleFeeRecipient; uint32 bpsFee; uint32 protocolFee; }; struct Propose_output { - uint8 proposalId; // slot index; valid only when success == true - bit success; + uint8 proposalId; // slot index; valid only when success == true + bit success; uint8 reasonCode; }; - struct ApproveProposal_input { uint8 proposalId; }; - struct ApproveProposal_output { bit success; bit executed; uint8 reasonCode; }; + struct ApproveProposal_input + { + uint8 proposalId; + }; + struct ApproveProposal_output + { + bit success; + bit executed; + uint8 reasonCode; + }; - struct CancelProposal_input { uint8 proposalId; }; - struct CancelProposal_output { bit success; uint8 reasonCode; }; + struct CancelProposal_input + { + uint8 proposalId; + }; + struct CancelProposal_output + { + bit success; + uint8 reasonCode; + }; - struct GetProposal_input { uint8 proposalId; }; - struct GetProposal_output { bit exists; AdminProposal proposal; }; + struct GetProposal_input + { + uint8 proposalId; + }; + struct GetProposal_output + { + bit exists; + AdminProposal proposal; + }; - struct GetProposals_input {}; + struct GetProposals_input + { + }; struct GetProposals_output { uint8 count; @@ -491,17 +514,17 @@ struct QSB : public ContractBase struct GetConfig_output { - uint8 adminCount; - uint8 adminThreshold; + uint8 adminCount; + uint8 adminThreshold; Array admins; - id protocolFeeRecipient; - id oracleFeeRecipient; + id protocolFeeRecipient; + id oracleFeeRecipient; uint32 bpsFee; uint32 protocolFee; uint32 oracleCount; uint32 pauserCount; - uint8 oracleThreshold; - bit paused; + uint8 oracleThreshold; + bit paused; uint32 orderEra; }; @@ -613,9 +636,9 @@ struct QSB : public ContractBase struct StateData { // Multisig admin (replaces single `id admin`) - Array admins; // zero entry = empty slot - uint8 adminCount; // number of active admins - uint8 adminThreshold; // M in M-of-N (always ≥ 1, always ≤ adminCount) + Array admins; // zero entry = empty slot + uint8 adminCount; // number of active admins + uint8 adminThreshold; // M in M-of-N (always ≥ 1, always ≤ adminCount) Array proposals; id protocolFeeRecipient; @@ -631,26 +654,22 @@ struct QSB : public ContractBase uint32 pauserCount; uint32 bpsFee; uint32 protocolFee; - uint8 oracleThreshold; // percent [1..100] - bit paused; + uint8 oracleThreshold; // percent [1..100] + bit paused; uint32 orderEra; }; protected: - // --------------------------------------------------------------------- - // Internal helpers + // Low-level helpers // --------------------------------------------------------------------- - // Truncate digest to OrderHash (full 32 bytes) - inline static void digestToOrderHash(const id& digest, OrderHash& outHash) + 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) + inline static void initDomainPrefix(QSBOrderMessage &msg) { setMemory(msg, 0); msg.protocolNameLen = 11; @@ -672,41 +691,45 @@ struct QSB : public ContractBase } inline static void buildOrderMessage( - QSBOrderMessage& msg, - const Order& order, - OrderHash& tmpIdBytes, - uint32 i) + QSBOrderMessage &msg, + const Order &order, + OrderHash &tmpIdBytes) { + uint32 i; initDomainPrefix(msg); msg.networkIn = order.networkIn; msg.networkOut = order.networkOut; - for (i = 0; i < 32; ++i) msg.tokenIn.set(i, order.tokenIn.get(i)); - for (i = 0; i < 32; ++i) msg.tokenOut.set(i, order.tokenOut.get(i)); + for (i = 0; i < 32; ++i) + msg.tokenIn.set(i, order.tokenIn.get(i)); + for (i = 0; i < 32; ++i) + msg.tokenOut.set(i, order.tokenOut.get(i)); tmpIdBytes.setMem(order.fromAddress); - for (i = 0; i < 32; ++i) msg.fromAddress.set(i, tmpIdBytes.get(i)); + for (i = 0; i < 32; ++i) + msg.fromAddress.set(i, tmpIdBytes.get(i)); tmpIdBytes.setMem(order.toAddress); - for (i = 0; i < 32; ++i) msg.toAddress.set(i, tmpIdBytes.get(i)); + for (i = 0; i < 32; ++i) + msg.toAddress.set(i, tmpIdBytes.get(i)); msg.amount = order.amount; msg.relayerFee = order.relayerFee; - for (i = 0; i < 32; ++i) msg.nonce.set(i, order.nonce.get(i)); + for (i = 0; i < 32; ++i) + msg.nonce.set(i, order.nonce.get(i)); msg.orderEra = order.orderEra; } - // Popcount for uint8 approvedMask (used by multisig approval tracking) - inline static uint8 countBitsUint8(uint8 mask, uint8 i) + inline static uint8 countBitsUint8(uint8 mask) { uint8 count = 0; - for (i = 0; i < 8; ++i) + for (uint8 i = 0; i < 8; ++i) { - if (mask & (uint8)(1u << i)) ++count; + if (mask & (uint8)(1u << i)) + ++count; } return count; } - // Check if caller is in the admin array - inline static bool isAdmin(const QPI::ContractState& state, const id& who, uint32 i) + inline static bool isAdmin(const QPI::ContractState &state, const id &who) { - for (i = 0; i < QSB_MAX_ADMINS; ++i) + for (uint32 i = 0; i < QSB_MAX_ADMINS; ++i) { if (!isZero(state.get().admins.get(i)) && state.get().admins.get(i) == who) return true; @@ -714,10 +737,9 @@ struct QSB : public ContractBase return false; } - // Find admin slot index; returns NULL_INDEX if not found - inline static sint64 findAdminIndex(const QPI::ContractState& state, const id& who, uint32 i) + inline static sint64 findAdminIndex(const QPI::ContractState &state, const id &who) { - for (i = 0; i < QSB_MAX_ADMINS; ++i) + for (uint32 i = 0; i < QSB_MAX_ADMINS; ++i) { if (!isZero(state.get().admins.get(i)) && state.get().admins.get(i) == who) return (sint64)i; @@ -725,13 +747,11 @@ struct QSB : public ContractBase return NULL_INDEX; } - // Check if caller is admin or has pauser role - inline static bool isAdminOrPauser(const QPI::ContractState& state, const id& who, uint32 i) + inline static bool isAdminOrPauser(const QPI::ContractState &state, const id &who) { - if (isAdmin(state, who, 0)) + if (isAdmin(state, who)) return true; - - for (i = 0; i < state.get().pausers.capacity(); ++i) + for (uint32 i = 0; i < state.get().pausers.capacity(); ++i) { if (state.get().pausers.get(i).active && state.get().pausers.get(i).account == who) return true; @@ -739,11 +759,11 @@ struct QSB : public ContractBase return false; } - // Cancel all pending (active) proposals — called when admin set changes - inline static void cancelAllPendingProposals(QPI::ContractState& state, uint32 i) + // Cancel all pending proposals — called when the admin set changes to invalidate stale votes. + inline static void cancelAllPendingProposals(QPI::ContractState &state) { AdminProposal prop; - for (i = 0; i < QSB_MAX_PROPOSALS; ++i) + for (uint32 i = 0; i < QSB_MAX_PROPOSALS; ++i) { prop = state.get().proposals.get(i); if (prop.active) @@ -754,10 +774,10 @@ struct QSB : public ContractBase } } - // Execute the payload of an approved proposal. Returns true on success. // Pure state mutation — no qpi access. - inline static bool executeProposalPayload(QPI::ContractState& state, const AdminProposal& prop, uint32 i) + inline static bool executeProposalPayload(QPI::ContractState &state, const AdminProposal &prop) { + uint32 i; RoleEntry entry; sint64 idx; @@ -776,7 +796,7 @@ struct QSB : public ContractBase } else if (prop.proposalType == QSBPropRemoveAdmin) { - idx = findAdminIndex(state, prop.targetId, 0); + idx = findAdminIndex(state, prop.targetId); state.mut().admins.set((uint32)idx, NULL_ID); state.mut().adminCount = state.get().adminCount - 1; return true; @@ -790,7 +810,7 @@ struct QSB : public ContractBase { if (prop.role == (uint8)Role::Oracle) { - if (findOracleIndex(state, prop.targetId, 0) != NULL_INDEX) + if (findOracleIndex(state, prop.targetId) != NULL_INDEX) return true; for (i = 0; i < state.get().oracles.capacity(); ++i) { @@ -798,7 +818,7 @@ struct QSB : public ContractBase if (!entry.active) { entry.account = prop.targetId; - entry.active = true; + entry.active = true; state.mut().oracles.set(i, entry); ++state.mut().oracleCount; return true; @@ -808,7 +828,7 @@ struct QSB : public ContractBase } else if (prop.role == (uint8)Role::Pauser) { - if (findPauserIndex(state, prop.targetId, 0) != NULL_INDEX) + if (findPauserIndex(state, prop.targetId) != NULL_INDEX) return true; for (i = 0; i < state.get().pausers.capacity(); ++i) { @@ -816,7 +836,7 @@ struct QSB : public ContractBase if (!entry.active) { entry.account = prop.targetId; - entry.active = true; + entry.active = true; state.mut().pausers.set(i, entry); ++state.mut().pauserCount; return true; @@ -830,24 +850,26 @@ struct QSB : public ContractBase { if (prop.role == (uint8)Role::Oracle) { - idx = findOracleIndex(state, prop.targetId, 0); + idx = findOracleIndex(state, prop.targetId); if (idx == NULL_INDEX) return true; entry = state.get().oracles.get((uint32)idx); entry.active = false; state.mut().oracles.set((uint32)idx, entry); - if (state.get().oracleCount > 0) --state.mut().oracleCount; + if (state.get().oracleCount > 0) + --state.mut().oracleCount; return true; } else if (prop.role == (uint8)Role::Pauser) { - idx = findPauserIndex(state, prop.targetId, 0); + idx = findPauserIndex(state, prop.targetId); if (idx == NULL_INDEX) return true; entry = state.get().pausers.get((uint32)idx); entry.active = false; state.mut().pausers.set((uint32)idx, entry); - if (state.get().pauserCount > 0) --state.mut().pauserCount; + if (state.get().pauserCount > 0) + --state.mut().pauserCount; return true; } return false; @@ -877,10 +899,9 @@ struct QSB : public ContractBase return false; } - // Find oracle index; returns NULL_INDEX if not found - inline static sint64 findOracleIndex(const QPI::ContractState& state, const id& account, uint32 i) + inline static sint64 findOracleIndex(const QPI::ContractState &state, const id &account) { - for (i = 0; i < state.get().oracles.capacity(); ++i) + for (uint32 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; @@ -888,10 +909,9 @@ struct QSB : public ContractBase 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) + inline static sint64 findPauserIndex(const QPI::ContractState &state, const id &account) { - for (i = 0; i < state.get().pausers.capacity(); ++i) + for (uint32 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; @@ -899,10 +919,13 @@ struct QSB : public ContractBase return NULL_INDEX; } - // 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) + // Idempotent insert into ring-buffer filled-order storage. + inline static void markOrderFilled(QPI::ContractState &state, const OrderHash &hash) { - // First, see if it already exists + uint32 i, j; + bool same; + FilledOrderEntry entry; + for (i = 0; i < state.get().filledOrders.capacity(); ++i) { entry = state.get().filledOrders.get(i); @@ -922,7 +945,6 @@ struct QSB : public ContractBase } } - // 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; @@ -939,15 +961,18 @@ struct QSB : public ContractBase } } - // Check whether an orderHash has already been filled (checks current and previous era buffers) - inline static bit isOrderFilled(const QPI::ContractState& state, const OrderHash& hash, uint32 i, uint32 j, bool same, FilledOrderEntry& entry) + // Checks current and previous era buffers to cover in-flight orders across a ring wrap. + inline static bit isOrderFilled(const QPI::ContractState &state, const OrderHash &hash) { + uint32 i, 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) { @@ -965,7 +990,6 @@ struct QSB : public ContractBase entry = state.get().filledOrdersPrev.get(i); if (!entry.used) continue; - same = true; for (j = 0; j < hash.capacity(); ++j) { @@ -981,10 +1005,9 @@ struct QSB : public ContractBase 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) + inline static sint64 findLockedOrderIndexByNonce(const QPI::ContractState &state, uint32 nonce) { - for (i = 0; i < QSB_MAX_LOCKED_ORDERS; ++i) + for (uint32 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; @@ -992,377 +1015,968 @@ struct QSB : public ContractBase return NULL_INDEX; } + // Sweep expired proposals — called from END_EPOCH. + inline static void sweepExpiredProposals( + const QPI::QpiContextProcedureCall &qpi, + QPI::ContractState &state) + { + AdminProposal prop; + for (uint32 i = 0; i < QSB_MAX_PROPOSALS; ++i) + { + prop = state.get().proposals.get(i); + if (prop.active && qpi.epoch() > prop.createdEpoch + QSB_PROPOSAL_EXPIRY_EPOCHS) + { + prop.active = 0; + state.mut().proposals.set(i, prop); + } + } + } -public: // --------------------------------------------------------------------- - // Core user procedures + // Procedure result structs // --------------------------------------------------------------------- - struct Lock_locals + struct LockResult { - id digest; - Order tmpOrder; - LockedOrderEntry entry; - QSBOrderMessage msgBuffer; - OrderHash tmpIdBytes; - uint32 i; - QSBLogLockMessage logMsg; + bit success; + uint8 reasonCode; + OrderHash orderHash; + uint32 orderEra; }; - PUBLIC_PROCEDURE_WITH_LOCALS(Lock) + struct OverrideLockResult { - 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; + bit success; + uint8 reasonCode; + OrderHash orderHash; + uint64 amount; + uint64 relayerFee; + uint32 networkOut; + uint32 orderEra; + }; + + struct UnlockResult + { + bit success; + uint8 reasonCode; + OrderHash orderHash; + }; + + struct ProposeResult + { + bit success; + uint8 reasonCode; + uint8 proposalId; + }; + + struct ApproveProposalResult + { + bit success; + bit executed; + uint8 reasonCode; + uint8 proposalType; + id proposer; + uint8 approvalCount; + }; + + struct CancelProposalResult + { + bit success; + uint8 reasonCode; + uint8 proposalType; + id proposer; + uint8 approvalCount; + }; + + struct PauseResult + { + bit success; + uint8 reasonCode; + }; + + // --------------------------------------------------------------------- + // Procedure logic helpers (try*) + // All logic lives here; LOG_INFO stays in the thin procedure shell. + // --------------------------------------------------------------------- - output.success = false; - setMemory(output.orderHash, 0); + inline static LockResult tryLock( + const QPI::QpiContextProcedureCall &qpi, + QPI::ContractState &state, + const Lock_input &input) + { + LockResult result = { false, QSBReasonNone, {}, 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; + result.reasonCode = QSBReasonPaused; + return result; } - // 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; + result.reasonCode = QSBReasonInvalidAmount; + return result; } - // 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; + result.reasonCode = QSBReasonInsufficientReward; + return result; } - // Any excess over `amount` is refunded + // Any excess over `amount` is refunded; exactly `amount` stays locked. 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) + if (findLockedOrderIndexByNonce(state, input.nonce) != 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 in ring buffer. Oldest slot is overwritten when the buffer is full; - // by the time the ring wraps (1024 orders), off-chain tooling has indexed earlier entries. - 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; - locals.entry.overrideLockCount = 0; - state.mut().lockedOrders.set(state.get().lastLockedOrdersNextOverwriteIdx, locals.entry); - 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); - } + result.reasonCode = QSBReasonNonceUsed; + return result; + } - struct OverrideLock_locals - { - LockedOrderEntry entry; Order tmpOrder; - id digest; + tmpOrder.networkIn = 1; + tmpOrder.networkOut = input.networkOut; + setMemory(tmpOrder.tokenIn, 0); + setMemory(tmpOrder.tokenOut, 0); + tmpOrder.fromAddress = qpi.invocator(); + tmpOrder.toAddress = NULL_ID; + tmpOrder.amount = input.amount; + tmpOrder.relayerFee = input.relayerFee; + setMemory(tmpOrder.nonce, 0); + tmpOrder.nonce.set(0, (uint8)(input.nonce & 0xFF)); + tmpOrder.nonce.set(1, (uint8)((input.nonce >> 8) & 0xFF)); + tmpOrder.nonce.set(2, (uint8)((input.nonce >> 16) & 0xFF)); + tmpOrder.nonce.set(3, (uint8)((input.nonce >> 24) & 0xFF)); + tmpOrder.orderEra = state.get().orderEra; + QSBOrderMessage msgBuffer; OrderHash tmpIdBytes; - sint64 idx; - uint32 i; - QSBLogOverrideLockMessage logMsg; - }; + buildOrderMessage(msgBuffer, tmpOrder, tmpIdBytes); + id digest = qpi.K12(msgBuffer); + digestToOrderHash(digest, result.orderHash); + result.orderEra = state.get().orderEra; - PUBLIC_PROCEDURE_WITH_LOCALS(OverrideLock) + // Persist in ring buffer; oldest slot overwritten when full. + // By the time the ring wraps (1024 orders), off-chain tooling has indexed earlier entries. + LockedOrderEntry entry; + entry.active = true; + entry.sender = qpi.invocator(); + entry.networkOut = input.networkOut; + entry.amount = input.amount; + entry.relayerFee = input.relayerFee; + entry.nonce = input.nonce; + copyMemory(entry.toAddress, input.toAddress); + entry.orderHash = result.orderHash; + entry.lockEpoch = qpi.epoch(); + entry.orderEra = state.get().orderEra; + entry.overrideLockCount = 0; + state.mut().lockedOrders.set(state.get().lastLockedOrdersNextOverwriteIdx, entry); + state.mut().lastLockedOrdersNextOverwriteIdx = + (state.get().lastLockedOrdersNextOverwriteIdx + 1) & (QSB_MAX_LOCKED_ORDERS - 1); + + result.success = true; + return result; + } + + inline static OverrideLockResult tryOverrideLock( + const QPI::QpiContextProcedureCall &qpi, + QPI::ContractState &state, + const OverrideLock_input &input) { - 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); + OverrideLockResult result = { false, QSBReasonNone, {}, 0, 0, 0, 0 }; - // Always refund invocationReward (locking was done in original lock() call) + // Always refund — locking was done in the 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; + result.reasonCode = QSBReasonPaused; + return result; } - // Find existing order by nonce - locals.idx = findLockedOrderIndexByNonce(state, input.nonce, 0); - if (locals.idx == NULL_INDEX) + sint64 idx = findLockedOrderIndexByNonce(state, input.nonce); + if (idx == NULL_INDEX) { - locals.logMsg.reasonCode = QSBReasonOrderNotFound; - LOG_INFO(locals.logMsg); - return; + result.reasonCode = QSBReasonOrderNotFound; + return result; } - locals.entry = state.get().lockedOrders.get((uint32)locals.idx); + LockedOrderEntry entry = state.get().lockedOrders.get((uint32)idx); - // Only original sender can override - if (locals.entry.sender != qpi.invocator()) + if (entry.sender != qpi.invocator()) { - locals.logMsg.reasonCode = QSBReasonNotSender; - LOG_INFO(locals.logMsg); - return; + result.reasonCode = QSBReasonNotSender; + return result; } - // Enforce per-order override attempt cap - if (locals.entry.overrideLockCount >= QSB_OVERRIDE_LOCK_MAX_ATTEMPTS) + if (entry.overrideLockCount >= QSB_OVERRIDE_LOCK_MAX_ATTEMPTS) { - locals.logMsg.reasonCode = QSBReasonOverrideLimitReached; - LOG_INFO(locals.logMsg); - return; + result.reasonCode = QSBReasonOverrideLimitReached; + return result; } - // Validate new relayer fee - if (input.relayerFee >= locals.entry.amount) + if (input.relayerFee >= entry.amount) { - locals.logMsg.reasonCode = QSBReasonBadRelayerFee; - LOG_INFO(locals.logMsg); - return; + result.reasonCode = QSBReasonBadRelayerFee; + return result; } - // Update mutable fields - copyMemory(locals.entry.toAddress, input.toAddress); - locals.entry.relayerFee = input.relayerFee; + copyMemory(entry.toAddress, input.toAddress); + 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 + Order tmpOrder; + tmpOrder.networkIn = 1; + tmpOrder.networkOut = entry.networkOut; + setMemory(tmpOrder.tokenIn, 0); + setMemory(tmpOrder.tokenOut, 0); + tmpOrder.fromAddress = entry.sender; + tmpOrder.toAddress = NULL_ID; + tmpOrder.amount = entry.amount; + tmpOrder.relayerFee = entry.relayerFee; + setMemory(tmpOrder.nonce, 0); + tmpOrder.nonce.set(0, (uint8)(entry.nonce & 0xFF)); + tmpOrder.nonce.set(1, (uint8)((entry.nonce >> 8) & 0xFF)); + tmpOrder.nonce.set(2, (uint8)((entry.nonce >> 16) & 0xFF)); + tmpOrder.nonce.set(3, (uint8)((entry.nonce >> 24) & 0xFF)); + tmpOrder.orderEra = 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; - - locals.entry.overrideLockCount++; - 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); + QSBOrderMessage msgBuffer; + OrderHash tmpIdBytes; + buildOrderMessage(msgBuffer, tmpOrder, tmpIdBytes); + id digest = qpi.K12(msgBuffer); + digestToOrderHash(digest, entry.orderHash); + + entry.overrideLockCount++; + state.mut().lockedOrders.set((uint32)idx, entry); + + result.orderHash = entry.orderHash; + result.amount = entry.amount; + result.relayerFee = entry.relayerFee; + result.networkOut = entry.networkOut; + result.orderEra = entry.orderEra; + result.success = true; + return result; } - // View helpers - PUBLIC_FUNCTION(GetConfig) + inline static UnlockResult tryUnlock( + const QPI::QpiContextProcedureCall &qpi, + QPI::ContractState &state, + const Unlock_input &input) { - output.adminCount = state.get().adminCount; - output.adminThreshold = state.get().adminThreshold; - output.admins = state.get().admins; - 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; - } + UnlockResult result = { false, QSBReasonNone, {} }; - PUBLIC_FUNCTION(GetProposal) - { - output.exists = false; - if (input.proposalId < QSB_MAX_PROPOSALS) + if (state.get().paused) { - output.proposal = state.get().proposals.get(input.proposalId); - output.exists = output.proposal.active; + if (qpi.invocationReward() > 0) + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + result.reasonCode = QSBReasonPaused; + return result; } - } - struct GetProposals_locals { uint32 i; AdminProposal prop; }; - PUBLIC_FUNCTION_WITH_LOCALS(GetProposals) - { - output.count = 0; - setMemory(output.proposals, 0); - for (locals.i = 0; locals.i < QSB_MAX_PROPOSALS; ++locals.i) + // Refund invocation reward — relayer is paid from order.amount, not from reward. + if (qpi.invocationReward() > 0) + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + if (input.order.amount == 0 || input.order.relayerFee >= input.order.amount) { - locals.prop = state.get().proposals.get(locals.i); - if (locals.prop.active) - { - output.proposals.set(output.count, locals.prop); - ++output.count; - } + result.reasonCode = QSBReasonInvalidAmount; + return result; } - } - - 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; - }; + // Defensive balance check — should never fail under normal operation since + // Lock keeps funds inside the contract, but guards against unexpected discrepancies. + Entity entity; + qpi.getEntity(SELF, entity); + uint64 contractBalance = (entity.incomingAmount >= entity.outgoingAmount) + ? entity.incomingAmount - entity.outgoingAmount + : 0; - PUBLIC_FUNCTION_WITH_LOCALS(GetLockedOrder) - { - locals.idx = findLockedOrderIndexByNonce(state, input.nonce, 0); - output.exists = (locals.idx != NULL_INDEX); - if (output.exists) + if (contractBalance < input.order.amount) { - output.order = state.get().lockedOrders.get((uint32)locals.idx); + result.reasonCode = QSBReasonInsufficientReward; + return result; } - } - struct IsOrderFilled_locals - { - FilledOrderEntry entry; - bool same; - }; + // Accept current era or immediately previous era. + // The N-1 grace window covers in-flight orders signed just before a ring-buffer wrap; + // isOrderFilled checks both buffers to prevent replays across the boundary. + if (input.order.orderEra != state.get().orderEra && + !(state.get().orderEra > 0 && input.order.orderEra == state.get().orderEra - 1)) + { + result.reasonCode = QSBReasonEraMismatch; + return result; + } - PUBLIC_FUNCTION_WITH_LOCALS(IsOrderFilled) - { - output.filled = isOrderFilled(state, input.hash, 0, 0, locals.same, locals.entry); - } + // 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 a balance check. + // This models a fungible lock/mint ↔ burn/unlock bridge where minted tokens + // can be freely transferred and aggregated. - struct ComputeOrderHash_locals - { - id digest; QSBOrderMessage msgBuffer; OrderHash tmpIdBytes; - uint32 i; - }; + buildOrderMessage(msgBuffer, input.order, tmpIdBytes); + id digest = qpi.K12(msgBuffer); + digestToOrderHash(digest, result.orderHash); - 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); - } + FilledOrderEntry entry; + if (isOrderFilled(state, result.orderHash)) + { + result.reasonCode = QSBReasonAlreadyFilled; + return result; + } - struct GetOracles_locals - { - uint32 i; - RoleEntry entry; - }; + if (state.get().oracleCount == 0 || input.numSignatures == 0) + { + result.reasonCode = QSBReasonNoOracles; + return result; + } - 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) + // requiredSignatures = ceil(oracleCount * oracleThreshold / 100) + uint128 tmpMul = uint128(state.get().oracleCount) * uint128(state.get().oracleThreshold); + uint128 tmpMul2 = div(tmpMul, uint128(100)); + uint32 requiredSignatures = (uint32)tmpMul2.low; + if (requiredSignatures * 100 < state.get().oracleCount * state.get().oracleThreshold) + ++requiredSignatures; + if (requiredSignatures == 0) + requiredSignatures = 1; + + uint32 validSignatureCount = 0; + uint32 seenCount = 0; + Array seenSigners; + + for (uint32 i = 0; i < input.numSignatures && i < input.signatures.capacity(); ++i) { - locals.entry = state.get().oracles.get(locals.i); - if (locals.entry.active) + SignatureData sig = input.signatures.get(i); + + if (findOracleIndex(state, sig.signer) == NULL_INDEX) { - output.accounts.set(output.count, locals.entry.account); - ++output.count; + result.reasonCode = QSBReasonInvalidSignature; + return result; } - } - } - - struct GetPausers_locals - { - uint32 i; - RoleEntry entry; - }; - PUBLIC_FUNCTION_WITH_LOCALS(GetPausers) + for (uint32 j = 0; j < seenCount; ++j) + { + if (seenSigners.get(j) == sig.signer) + { + result.reasonCode = QSBReasonDuplicateSigner; + return result; + } + } + + if (!qpi.signatureValidity(sig.signer, digest, sig.signature)) + { + result.reasonCode = QSBReasonInvalidSignature; + return result; + } + + if (seenCount < seenSigners.capacity()) + { + seenSigners.set(seenCount, sig.signer); + ++seenCount; + } + ++validSignatureCount; + } + + if (validSignatureCount < requiredSignatures) + { + result.reasonCode = QSBReasonThresholdFailed; + return result; + } + + // bpsFeeAmount = netAmount * bpsFee / 10000 + uint64 netAmount = input.order.amount - input.order.relayerFee; + tmpMul = uint128(netAmount) * uint128(state.get().bpsFee); + tmpMul2 = div(tmpMul, uint128(10000)); + uint64 bpsFeeAmount = (uint64)tmpMul2.low; + + // protocolFeeAmount = bpsFeeAmount * protocolFee / 100 + tmpMul = uint128(bpsFeeAmount) * uint128(state.get().protocolFee); + tmpMul2 = div(tmpMul, uint128(100)); + uint64 protocolFeeAmount = (uint64)tmpMul2.low; + + // oracleFeeAmount = bpsFeeAmount - protocolFeeAmount + uint64 oracleFeeAmount = (bpsFeeAmount >= protocolFeeAmount) ? bpsFeeAmount - protocolFeeAmount : 0; + + // recipientAmount = netAmount - bpsFeeAmount + uint64 recipientAmount = (netAmount >= bpsFeeAmount) ? netAmount - bpsFeeAmount : 0; + + // Mark filled BEFORE transfers to prevent replay on partial transfer failure. + // The balance check above guarantees the contract has enough funds. + markOrderFilled(state, result.orderHash); + + bool allTransfersOk = true; + + // Recipient payout first (most important transfer) + if (recipientAmount > 0 && !isZero(input.order.toAddress)) + { + if (qpi.transfer(input.order.toAddress, (sint64)recipientAmount) < 0) + allTransfersOk = false; + } + + if (input.order.relayerFee > 0) + { + if (qpi.transfer(qpi.invocator(), (sint64)input.order.relayerFee) < 0) + allTransfersOk = false; + } + + if (protocolFeeAmount > 0 && !isZero(state.get().protocolFeeRecipient)) + { + if (qpi.transfer(state.get().protocolFeeRecipient, (sint64)protocolFeeAmount) < 0) + allTransfersOk = false; + } + + if (oracleFeeAmount > 0 && !isZero(state.get().oracleFeeRecipient)) + { + if (qpi.transfer(state.get().oracleFeeRecipient, (sint64)oracleFeeAmount) < 0) + allTransfersOk = false; + } + + if (!allTransfersOk) + { + result.reasonCode = QSBReasonTransferFailed; + return result; + } + + result.success = true; + return result; + } + + inline static ProposeResult tryPropose( + const QPI::QpiContextProcedureCall &qpi, + QPI::ContractState &state, + const Propose_input &input) + { + ProposeResult result = { false, QSBReasonNone, 0 }; + + if (qpi.invocationReward() > 0) + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + sint64 adminIdx = findAdminIndex(state, qpi.invocator()); + if (adminIdx == NULL_INDEX) + { + result.reasonCode = QSBReasonNotAdmin; + return result; + } + + if (input.proposalType == 0 || input.proposalType > QSBPropUnpause) + { + result.reasonCode = QSBReasonInvalidRole; + return result; + } + + if (input.proposalType == QSBPropAddAdmin) + { + if (isZero(input.targetId)) + { + result.reasonCode = QSBReasonInvalidAdmin; + return result; + } + if (findAdminIndex(state, input.targetId) != NULL_INDEX) + { + result.reasonCode = QSBReasonAlreadyAdmin; + return result; + } + if (state.get().adminCount >= QSB_MAX_ADMINS) + { + result.reasonCode = QSBReasonAdminFull; + return result; + } + } + else if (input.proposalType == QSBPropRemoveAdmin) + { + if (isZero(input.targetId)) + { + result.reasonCode = QSBReasonInvalidAdmin; + return result; + } + if (findAdminIndex(state, input.targetId) == NULL_INDEX) + { + result.reasonCode = QSBReasonRoleMissing; + return result; + } + if (state.get().adminCount <= 1) + { + result.reasonCode = QSBReasonWouldLockContract; + return result; + } + if ((state.get().adminCount - 1) < state.get().adminThreshold) + { + result.reasonCode = QSBReasonWouldLockContract; + return result; + } + } + else if (input.proposalType == QSBPropSetAdminThreshold) + { + if (input.newAdminThreshold == 0 || input.newAdminThreshold > state.get().adminCount) + { + result.reasonCode = QSBReasonInvalidThreshold; + return result; + } + } + else if (input.proposalType == QSBPropAddRole || input.proposalType == QSBPropRemoveRole) + { + if (isZero(input.targetId)) + { + result.reasonCode = QSBReasonInvalidAdmin; + return result; + } + if (input.role != (uint8)Role::Oracle && input.role != (uint8)Role::Pauser) + { + result.reasonCode = QSBReasonInvalidRole; + return result; + } + } + else if (input.proposalType == QSBPropEditOracleThreshold) + { + if (input.newOracleThreshold == 0 || input.newOracleThreshold > 100) + { + result.reasonCode = QSBReasonInvalidThreshold; + return result; + } + } + else if (input.proposalType == QSBPropEditFeeParameters) + { + if (input.bpsFee > QSB_MAX_BPS_FEE || input.protocolFee > QSB_MAX_PROTOCOL_FEE) + { + result.reasonCode = QSBReasonInvalidFeeParams; + return result; + } + } + else if (input.proposalType != QSBPropUnpause) + { + result.reasonCode = QSBReasonInvalidProposalType; + return result; + } + + // Enforce per-admin concurrent proposal cap + uint8 adminProposalCount = 0; + AdminProposal prop; + for (uint32 i = 0; i < QSB_MAX_PROPOSALS; ++i) + { + prop = state.get().proposals.get(i); + if (prop.active && prop.proposer == qpi.invocator()) + ++adminProposalCount; + } + if (adminProposalCount >= QSB_MAX_PROPOSALS_PER_ADMIN) + { + result.reasonCode = QSBReasonTooManyProposals; + return result; + } + + uint8 slotIdx = (uint8)QSB_MAX_PROPOSALS; + for (uint32 i = 0; i < QSB_MAX_PROPOSALS; ++i) + { + if (!state.get().proposals.get(i).active) + { + slotIdx = (uint8)i; + break; + } + } + if (slotIdx >= QSB_MAX_PROPOSALS) + { + result.reasonCode = QSBReasonProposalFull; + return result; + } + + // Build proposal; proposer auto-approves (bit set at their admin index). + setMemory(prop, 0); + prop.proposalType = input.proposalType; + prop.active = 1; + prop.executed = 0; + prop.proposer = qpi.invocator(); + prop.createdEpoch = qpi.epoch(); + prop.approvedMask = (uint8)(1u << (uint8)adminIdx); + prop.approvalCount = 1; + prop.targetId = input.targetId; + prop.role = input.role; + prop.newAdminThreshold = input.newAdminThreshold; + prop.newOracleThreshold = input.newOracleThreshold; + prop.protocolFeeRecipient = input.protocolFeeRecipient; + prop.oracleFeeRecipient = input.oracleFeeRecipient; + prop.bpsFee = input.bpsFee; + prop.protocolFee = input.protocolFee; + state.mut().proposals.set(slotIdx, prop); + result.proposalId = slotIdx; + result.success = true; + + // Execute immediately when threshold == 1 (single-admin or bootstrap mode). + if (state.get().adminThreshold <= 1) + { + bool execOk = executeProposalPayload(state, prop); + prop = state.get().proposals.get(slotIdx); + prop.active = 0; + prop.executed = execOk ? 1 : 0; + state.mut().proposals.set(slotIdx, prop); + if (execOk && + (input.proposalType == QSBPropAddAdmin || + input.proposalType == QSBPropRemoveAdmin || + input.proposalType == QSBPropSetAdminThreshold)) + { + cancelAllPendingProposals(state); + } + result.success = execOk; + } + + return result; + } + + inline static ApproveProposalResult tryApproveProposal( + const QPI::QpiContextProcedureCall &qpi, + QPI::ContractState &state, + const ApproveProposal_input &input) + { + ApproveProposalResult result = { false, false, QSBReasonNone, 0, NULL_ID, 0 }; + + if (qpi.invocationReward() > 0) + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + sint64 adminIdx = findAdminIndex(state, qpi.invocator()); + if (adminIdx == NULL_INDEX) + { + result.reasonCode = QSBReasonNotAdmin; + return result; + } + + if (input.proposalId >= QSB_MAX_PROPOSALS) + { + result.reasonCode = QSBReasonProposalNotFound; + return result; + } + + AdminProposal prop = state.get().proposals.get(input.proposalId); + result.proposalType = prop.proposalType; + result.proposer = prop.proposer; + result.approvalCount = prop.approvalCount; + + if (!prop.active) + { + result.reasonCode = QSBReasonProposalNotFound; + return result; + } + + if (qpi.epoch() > prop.createdEpoch + QSB_PROPOSAL_EXPIRY_EPOCHS) + { + prop.active = 0; + state.mut().proposals.set(input.proposalId, prop); + result.reasonCode = QSBReasonProposalExpired; + return result; + } + + uint8 bitPos = (uint8)adminIdx; + if (bitPos < 8 && (prop.approvedMask & (uint8)(1u << bitPos))) + { + result.reasonCode = QSBReasonAlreadyApproved; + return result; + } + + prop.approvedMask |= (uint8)(1u << bitPos); + prop.approvalCount = countBitsUint8(prop.approvedMask); + state.mut().proposals.set(input.proposalId, prop); + result.success = true; + result.approvalCount = prop.approvalCount; + + if (prop.approvalCount >= state.get().adminThreshold) + { + uint8 propType = prop.proposalType; + bool execOk = executeProposalPayload(state, prop); + prop = state.get().proposals.get(input.proposalId); + prop.active = 0; + prop.executed = execOk ? 1 : 0; + state.mut().proposals.set(input.proposalId, prop); + result.executed = true; + if (execOk && + (propType == QSBPropAddAdmin || + propType == QSBPropRemoveAdmin || + propType == QSBPropSetAdminThreshold)) + { + cancelAllPendingProposals(state); + } + } + + return result; + } + + inline static CancelProposalResult tryCancelProposal( + const QPI::QpiContextProcedureCall &qpi, + QPI::ContractState &state, + const CancelProposal_input &input) + { + CancelProposalResult result = { false, QSBReasonNone, 0, NULL_ID, 0 }; + + if (qpi.invocationReward() > 0) + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + if (!isAdmin(state, qpi.invocator())) + { + result.reasonCode = QSBReasonNotAdmin; + return result; + } + + if (input.proposalId >= QSB_MAX_PROPOSALS) + { + result.reasonCode = QSBReasonProposalNotFound; + return result; + } + + AdminProposal prop = state.get().proposals.get(input.proposalId); + result.proposalType = prop.proposalType; + result.proposer = prop.proposer; + result.approvalCount = prop.approvalCount; + + if (!prop.active) + { + result.reasonCode = QSBReasonProposalNotFound; + return result; + } + + if (prop.proposer != qpi.invocator()) + { + result.reasonCode = QSBReasonNotProposer; + return result; + } + + prop.active = 0; + state.mut().proposals.set(input.proposalId, prop); + result.success = true; + return result; + } + + inline static PauseResult tryPause( + const QPI::QpiContextProcedureCall &qpi, + QPI::ContractState &state) + { + PauseResult result = { false, QSBReasonNone }; + + if (qpi.invocationReward() > 0) + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + if (!isAdminOrPauser(state, qpi.invocator())) + { + result.reasonCode = QSBReasonNotAdminOrPauser; + return result; + } + + state.mut().paused = true; + result.success = true; + return result; + } + +public: + // --------------------------------------------------------------------- + // Core user procedures + // --------------------------------------------------------------------- + + struct Lock_locals + { + LockResult result; + QSBLogLockMessage logMsg; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(Lock) + { + locals.result = tryLock(qpi, state, input); + output.success = locals.result.success; + output.orderHash = locals.result.orderHash; + + 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; + locals.logMsg.orderHash = locals.result.orderHash; + locals.logMsg.success = locals.result.success ? 1 : 0; + locals.logMsg.reasonCode = locals.result.reasonCode; + locals.logMsg.orderEra = locals.result.orderEra; + locals.logMsg._terminator = 0; + LOG_INFO(locals.logMsg); + } + + struct OverrideLock_locals + { + OverrideLockResult result; + QSBLogOverrideLockMessage logMsg; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(OverrideLock) + { + locals.result = tryOverrideLock(qpi, state, input); + output.success = locals.result.success; + output.orderHash = locals.result.orderHash; + + locals.logMsg._contractIndex = SELF_INDEX; + locals.logMsg._type = QSBLogOverrideLock; + locals.logMsg.from = qpi.invocator(); + setMemory(locals.logMsg.to, 0); + if (locals.result.success) + copyFromBuffer(locals.logMsg.to, input.toAddress); + locals.logMsg.amount = locals.result.amount; + locals.logMsg.relayerFee = locals.result.relayerFee; + locals.logMsg.networkOut = locals.result.networkOut; + locals.logMsg.nonce = input.nonce; + locals.logMsg.orderHash = locals.result.orderHash; + locals.logMsg.success = locals.result.success ? 1 : 0; + locals.logMsg.reasonCode = locals.result.reasonCode; + locals.logMsg.orderEra = locals.result.orderEra; + locals.logMsg._terminator = 0; + LOG_INFO(locals.logMsg); + } + + struct Unlock_locals + { + UnlockResult result; + QSBLogUnlockMessage logMsg; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(Unlock) + { + locals.result = tryUnlock(qpi, state, input); + output.success = locals.result.success; + output.orderHash = locals.result.orderHash; + + locals.logMsg._contractIndex = SELF_INDEX; + locals.logMsg._type = QSBLogUnlock; + locals.logMsg.orderHash = locals.result.orderHash; + 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.success = locals.result.success ? 1 : 0; + locals.logMsg.reasonCode = locals.result.reasonCode; + locals.logMsg.orderEra = input.order.orderEra; + locals.logMsg._terminator = 0; + LOG_INFO(locals.logMsg); + } + + // View functions + PUBLIC_FUNCTION(GetConfig) + { + output.adminCount = state.get().adminCount; + output.adminThreshold = state.get().adminThreshold; + output.admins = state.get().admins; + 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(GetProposal) + { + output.exists = false; + if (input.proposalId < QSB_MAX_PROPOSALS) + { + output.proposal = state.get().proposals.get(input.proposalId); + output.exists = output.proposal.active; + } + } + + struct GetProposals_locals + { + uint32 i; + AdminProposal prop; + }; + PUBLIC_FUNCTION_WITH_LOCALS(GetProposals) + { + output.count = 0; + setMemory(output.proposals, 0); + for (locals.i = 0; locals.i < QSB_MAX_PROPOSALS; ++locals.i) + { + locals.prop = state.get().proposals.get(locals.i); + if (locals.prop.active) + { + output.proposals.set(output.count, locals.prop); + ++output.count; + } + } + } + + PUBLIC_FUNCTION(IsOracle) + { + output.isOracle = (findOracleIndex(state, input.account) != NULL_INDEX); + } + + PUBLIC_FUNCTION(IsPauser) + { + output.isPauser = (findPauserIndex(state, input.account) != NULL_INDEX); + } + + struct GetLockedOrder_locals + { + sint64 idx; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetLockedOrder) + { + locals.idx = findLockedOrderIndexByNonce(state, input.nonce); + output.exists = (locals.idx != NULL_INDEX); + if (output.exists) + output.order = state.get().lockedOrders.get((uint32)locals.idx); + } + + PUBLIC_FUNCTION(IsOrderFilled) + { + output.filled = isOrderFilled(state, input.hash); + } + + struct ComputeOrderHash_locals + { + id digest; + QSBOrderMessage msgBuffer; + OrderHash tmpIdBytes; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(ComputeOrderHash) + { + buildOrderMessage(locals.msgBuffer, input.order, locals.tmpIdBytes); + 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); @@ -1453,598 +2067,103 @@ struct QSB : public ContractBase 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: accept current era or the immediately previous era. - // Accepting era N-1 provides a grace window for in-flight orders signed just before - // a ring-buffer wrap, while isOrderFilled checks both buffers to prevent replays. - if (input.order.orderEra != state.get().orderEra && - !(state.get().orderEra > 0 && input.order.orderEra == state.get().orderEra - 1)) - { - locals.logMsg.reasonCode = QSBReasonEraMismatch; - LOG_INFO(locals.logMsg); - return; - } - - // NOTE: We intentionally do not require a matching lock() entry here. - // Unlock is driven solely by: - // - oracle signatures over the burn/unlock order (on the other chain), - // - replay protection via filledOrders, - // - and balance checks on this contract. - // This matches a fungible lock/mint ↔ burn/unlock bridge model where - // minted tokens can be freely transferred and aggregated, and where - // individual locks are not tied 1:1 to specific unlocks. - - // Serialize order with domain prefix and compute K12 digest - buildOrderMessage(locals.msgBuffer, input.order, locals.tmpIdBytes, locals.i); - locals.digest = qpi.K12(locals.msgBuffer); - digestToOrderHash(locals.digest, locals.hash); - output.orderHash = locals.hash; - locals.logMsg.orderHash = locals.hash; - - // Ensure orderHash not yet filled - if (isOrderFilled(state, locals.hash, 0, 0, 0, locals.entry)) - { - locals.logMsg.reasonCode = QSBReasonAlreadyFilled; - LOG_INFO(locals.logMsg); - return; - } - - // Verify oracle signatures against threshold - if (state.get().oracleCount == 0 || input.numSignatures == 0) - { - locals.logMsg.reasonCode = QSBReasonNoOracles; - LOG_INFO(locals.logMsg); - return; - } - - // requiredSignatures = ceil(oracleCount * oracleThreshold / 100) - locals.tmpMul = uint128(state.get().oracleCount) * uint128(state.get().oracleThreshold); - locals.tmpMul2 = div(locals.tmpMul, uint128(100)); - locals.requiredSignatures = (uint32)locals.tmpMul2.low; - if (locals.requiredSignatures * 100 < state.get().oracleCount * state.get().oracleThreshold) - { - ++locals.requiredSignatures; - } - if (locals.requiredSignatures == 0) - { - locals.requiredSignatures = 1; - } - - locals.validSignatureCount = 0; - locals.seenCount = 0; - - for (locals.i = 0; locals.i < input.numSignatures && locals.i < input.signatures.capacity(); ++locals.i) - { - locals.sig = input.signatures.get(locals.i); - - // Check signer is authorized oracle - if (findOracleIndex(state, locals.sig.signer, 0) == NULL_INDEX) - { - locals.logMsg.reasonCode = QSBReasonInvalidSignature; - LOG_INFO(locals.logMsg); // unknown signer -> fail fast - return; - } - - // Check duplicates - for (locals.j = 0; locals.j < locals.seenCount; ++locals.j) - { - if (locals.seenSigners.get(locals.j) == locals.sig.signer) - { - locals.logMsg.reasonCode = QSBReasonDuplicateSigner; - LOG_INFO(locals.logMsg); // duplicate signer -> fail - return; - } - } - - // Verify signature - if (!qpi.signatureValidity(locals.sig.signer, locals.digest, locals.sig.signature)) - { - locals.logMsg.reasonCode = QSBReasonInvalidSignature; - LOG_INFO(locals.logMsg); - return; - } - - // Record signer and increment count - if (locals.seenCount < locals.seenSigners.capacity()) - { - locals.seenSigners.set(locals.seenCount, locals.sig.signer); - ++locals.seenCount; - } - ++locals.validSignatureCount; - } - - if (locals.validSignatureCount < locals.requiredSignatures) - { - locals.logMsg.reasonCode = QSBReasonThresholdFailed; - LOG_INFO(locals.logMsg); - return; - } - - // ----------------------------------------------------------------- - // Fee calculations - // ----------------------------------------------------------------- - locals.netAmount = input.order.amount - input.order.relayerFee; - - // bpsFeeAmount = netAmount * bpsFee / 10000 - locals.tmpMul = uint128(locals.netAmount) * uint128(state.get().bpsFee); - locals.tmpMul2 = div(locals.tmpMul, uint128(10000)); - locals.bpsFeeAmount = (uint64)locals.tmpMul2.low; - - // protocolFeeAmount = bpsFeeAmount * protocolFee / 100 - locals.tmpMul = uint128(locals.bpsFeeAmount) * uint128(state.get().protocolFee); - locals.tmpMul2 = div(locals.tmpMul, uint128(100)); - locals.protocolFeeAmount = (uint64)locals.tmpMul2.low; - - // oracleFeeAmount = bpsFeeAmount - protocolFeeAmount - if (locals.bpsFeeAmount >= locals.protocolFeeAmount) - locals.oracleFeeAmount = locals.bpsFeeAmount - locals.protocolFeeAmount; - else - locals.oracleFeeAmount = 0; - - // recipientAmount = netAmount - bpsFeeAmount - if (locals.netAmount >= locals.bpsFeeAmount) - locals.recipientAmount = locals.netAmount - locals.bpsFeeAmount; - else - locals.recipientAmount = 0; - - // ----------------------------------------------------------------- - // Mark order as filled BEFORE transfers to prevent replay. - // If a transfer fails below, the order stays filled (no double-pay). - // The balance check above guarantees the contract has enough funds. - // ----------------------------------------------------------------- - markOrderFilled(state, locals.hash, 0, 0, 0, locals.entry); - - // ----------------------------------------------------------------- - // Token transfers - // ----------------------------------------------------------------- - - locals.allTransfersOk = true; - - // Recipient payout first (most important transfer) - if (locals.recipientAmount > 0 && !isZero(input.order.toAddress)) - { - if (qpi.transfer(input.order.toAddress, (sint64)locals.recipientAmount) < 0) - { - locals.allTransfersOk = false; - } - } - - // Relayer fee to caller - if (input.order.relayerFee > 0) - { - if (qpi.transfer(qpi.invocator(), (sint64)input.order.relayerFee) < 0) - { - locals.allTransfersOk = false; - } - } - - // Protocol fee - if (locals.protocolFeeAmount > 0 && !isZero(state.get().protocolFeeRecipient)) - { - if (qpi.transfer(state.get().protocolFeeRecipient, (sint64)locals.protocolFeeAmount) < 0) - { - locals.allTransfersOk = false; - } - } - - // Oracle fee - if (locals.oracleFeeAmount > 0 && !isZero(state.get().oracleFeeRecipient)) - { - if (qpi.transfer(state.get().oracleFeeRecipient, (sint64)locals.oracleFeeAmount) < 0) - { - locals.allTransfersOk = false; - } - } - - if (!locals.allTransfersOk) - { - locals.logMsg.reasonCode = QSBReasonTransferFailed; - LOG_INFO(locals.logMsg); - return; - } - - output.success = true; - locals.logMsg.success = 1; - locals.logMsg.reasonCode = QSBReasonNone; - LOG_INFO(locals.logMsg); - } - // --------------------------------------------------------------------- // Admin procedures (multisig) // --------------------------------------------------------------------- struct Propose_locals { - sint64 adminIdx; - uint32 i; - uint8 slotIdx; - uint8 adminProposalCount; - AdminProposal prop; - bool execOk; + ProposeResult result; QSBLogProposalMessage logMsg; }; PUBLIC_PROCEDURE_WITH_LOCALS(Propose) { - output.success = false; - output.proposalId = 0; - output.reasonCode = QSBReasonNone; - - if (qpi.invocationReward() > 0) - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - - locals.adminIdx = findAdminIndex(state, qpi.invocator(), 0); - if (locals.adminIdx == NULL_INDEX) - { output.reasonCode = QSBReasonNotAdmin; return; } - - if (input.proposalType == 0 || input.proposalType > QSBPropUnpause) - { output.reasonCode = QSBReasonInvalidRole; return; } - - // Per-type payload validation - if (input.proposalType == QSBPropAddAdmin) - { - if (isZero(input.targetId)) - { output.reasonCode = QSBReasonInvalidAdmin; return; } - if (findAdminIndex(state, input.targetId, 0) != NULL_INDEX) - { output.reasonCode = QSBReasonAlreadyAdmin; return; } - if (state.get().adminCount >= QSB_MAX_ADMINS) - { output.reasonCode = QSBReasonAdminFull; return; } - } - else if (input.proposalType == QSBPropRemoveAdmin) - { - if (isZero(input.targetId)) - { output.reasonCode = QSBReasonInvalidAdmin; return; } - if (findAdminIndex(state, input.targetId, 0) == NULL_INDEX) - { output.reasonCode = QSBReasonRoleMissing; return; } - if (state.get().adminCount <= 1) - { output.reasonCode = QSBReasonWouldLockContract; return; } - if ((state.get().adminCount - 1) < state.get().adminThreshold) - { output.reasonCode = QSBReasonWouldLockContract; return; } - } - else if (input.proposalType == QSBPropSetAdminThreshold) - { - if (input.newAdminThreshold == 0 || input.newAdminThreshold > state.get().adminCount) - { output.reasonCode = QSBReasonInvalidThreshold; return; } - } - else if (input.proposalType == QSBPropAddRole || input.proposalType == QSBPropRemoveRole) - { - if (isZero(input.targetId)) - { output.reasonCode = QSBReasonInvalidAdmin; return; } - if (input.role != (uint8)Role::Oracle && input.role != (uint8)Role::Pauser) - { output.reasonCode = QSBReasonInvalidRole; return; } - } - else if (input.proposalType == QSBPropEditOracleThreshold) - { - if (input.newOracleThreshold == 0 || input.newOracleThreshold > 100) - { output.reasonCode = QSBReasonInvalidThreshold; return; } - } - else if (input.proposalType == QSBPropEditFeeParameters) - { - if (input.bpsFee > QSB_MAX_BPS_FEE || input.protocolFee > QSB_MAX_PROTOCOL_FEE) - { output.reasonCode = QSBReasonInvalidFeeParams; return; } - } - else if (input.proposalType != QSBPropUnpause) - { output.reasonCode = QSBReasonInvalidProposalType; return; } - - // Enforce per-admin concurrent proposal cap - locals.adminProposalCount = 0; - for (locals.i = 0; locals.i < QSB_MAX_PROPOSALS; ++locals.i) - { - locals.prop = state.get().proposals.get(locals.i); - if (locals.prop.active && locals.prop.proposer == qpi.invocator()) - ++locals.adminProposalCount; - } - if (locals.adminProposalCount >= QSB_MAX_PROPOSALS_PER_ADMIN) - { output.reasonCode = QSBReasonTooManyProposals; return; } - - // Find free proposal slot - locals.slotIdx = (uint8)QSB_MAX_PROPOSALS; - for (locals.i = 0; locals.i < QSB_MAX_PROPOSALS; ++locals.i) - { - if (!state.get().proposals.get(locals.i).active) - { locals.slotIdx = (uint8)locals.i; break; } - } - if (locals.slotIdx >= QSB_MAX_PROPOSALS) - { output.reasonCode = QSBReasonProposalFull; return; } - - // Build proposal; proposer auto-approves - setMemory(locals.prop, 0); - locals.prop.proposalType = input.proposalType; - locals.prop.active = 1; - locals.prop.executed = 0; - locals.prop.proposer = qpi.invocator(); - locals.prop.createdEpoch = qpi.epoch(); - locals.prop.approvedMask = (uint8)(1u << (uint8)locals.adminIdx); - locals.prop.approvalCount = 1; - locals.prop.targetId = input.targetId; - locals.prop.role = input.role; - locals.prop.newAdminThreshold = input.newAdminThreshold; - locals.prop.newOracleThreshold = input.newOracleThreshold; - locals.prop.protocolFeeRecipient = input.protocolFeeRecipient; - locals.prop.oracleFeeRecipient = input.oracleFeeRecipient; - locals.prop.bpsFee = input.bpsFee; - locals.prop.protocolFee = input.protocolFee; - state.mut().proposals.set(locals.slotIdx, locals.prop); - output.proposalId = locals.slotIdx; - output.success = true; - - // Execute immediately when threshold == 1 (single-admin or bootstrap mode) - if (state.get().adminThreshold <= 1) - { - locals.execOk = executeProposalPayload(state, locals.prop, 0); - locals.prop = state.get().proposals.get(locals.slotIdx); - locals.prop.active = 0; - locals.prop.executed = locals.execOk ? 1 : 0; - state.mut().proposals.set(locals.slotIdx, locals.prop); - if (locals.execOk && - (input.proposalType == QSBPropAddAdmin || - input.proposalType == QSBPropRemoveAdmin || - input.proposalType == QSBPropSetAdminThreshold)) - { - cancelAllPendingProposals(state, 0); - } - output.success = locals.execOk; - } + locals.result = tryPropose(qpi, state, input); + output.success = locals.result.success; + output.reasonCode = locals.result.reasonCode; + output.proposalId = locals.result.proposalId; locals.logMsg._contractIndex = SELF_INDEX; - locals.logMsg._type = QSBLogProposalCreated; - locals.logMsg.proposalId = output.proposalId; - locals.logMsg.proposalType = input.proposalType; - locals.logMsg.proposer = qpi.invocator(); - locals.logMsg.actor = qpi.invocator(); - locals.logMsg.approvalCount = 1; - locals.logMsg.success = output.success ? 1 : 0; - locals.logMsg.reasonCode = output.reasonCode; - locals.logMsg._terminator = 0; + locals.logMsg._type = QSBLogProposalCreated; + locals.logMsg.proposalId = output.proposalId; + locals.logMsg.proposalType = input.proposalType; + locals.logMsg.proposer = qpi.invocator(); + locals.logMsg.actor = qpi.invocator(); + locals.logMsg.approvalCount = 1; + locals.logMsg.success = output.success ? 1 : 0; + locals.logMsg.reasonCode = output.reasonCode; + locals.logMsg._terminator = 0; LOG_INFO(locals.logMsg); } struct ApproveProposal_locals { - sint64 adminIdx; - uint8 bitPos; - uint8 propType; - AdminProposal prop; - bool execOk; + ApproveProposalResult result; QSBLogProposalMessage logMsg; }; PUBLIC_PROCEDURE_WITH_LOCALS(ApproveProposal) { - output.success = false; - output.executed = false; - output.reasonCode = QSBReasonNone; - - if (qpi.invocationReward() > 0) - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - - locals.adminIdx = findAdminIndex(state, qpi.invocator(), 0); - if (locals.adminIdx == NULL_INDEX) - { output.reasonCode = QSBReasonNotAdmin; return; } - - if (input.proposalId >= QSB_MAX_PROPOSALS) - { output.reasonCode = QSBReasonProposalNotFound; return; } - - locals.prop = state.get().proposals.get(input.proposalId); - - if (!locals.prop.active) - { output.reasonCode = QSBReasonProposalNotFound; return; } - - if (qpi.epoch() > locals.prop.createdEpoch + QSB_PROPOSAL_EXPIRY_EPOCHS) - { - locals.prop.active = 0; - state.mut().proposals.set(input.proposalId, locals.prop); - output.reasonCode = QSBReasonProposalExpired; - return; - } - - locals.bitPos = (uint8)locals.adminIdx; - if (locals.bitPos < 8 && (locals.prop.approvedMask & (uint8)(1u << locals.bitPos))) - { output.reasonCode = QSBReasonAlreadyApproved; return; } - - locals.prop.approvedMask |= (uint8)(1u << locals.bitPos); - locals.prop.approvalCount = countBitsUint8(locals.prop.approvedMask, 0); - state.mut().proposals.set(input.proposalId, locals.prop); - output.success = true; - - if (locals.prop.approvalCount >= state.get().adminThreshold) - { - locals.propType = locals.prop.proposalType; - locals.execOk = executeProposalPayload(state, locals.prop, 0); - locals.prop = state.get().proposals.get(input.proposalId); - locals.prop.active = 0; - locals.prop.executed = locals.execOk ? 1 : 0; - state.mut().proposals.set(input.proposalId, locals.prop); - output.executed = true; - if (locals.execOk && - (locals.propType == QSBPropAddAdmin || - locals.propType == QSBPropRemoveAdmin || - locals.propType == QSBPropSetAdminThreshold)) - { - cancelAllPendingProposals(state, 0); - } - } + locals.result = tryApproveProposal(qpi, state, input); + output.success = locals.result.success; + output.executed = locals.result.executed; + output.reasonCode = locals.result.reasonCode; locals.logMsg._contractIndex = SELF_INDEX; - locals.logMsg._type = output.executed ? QSBLogProposalExecuted : QSBLogProposalApproved; - locals.logMsg.proposalId = input.proposalId; - locals.logMsg.proposalType = locals.prop.proposalType; - locals.logMsg.proposer = locals.prop.proposer; - locals.logMsg.actor = qpi.invocator(); - locals.logMsg.approvalCount = locals.prop.approvalCount; - locals.logMsg.success = output.success ? 1 : 0; - locals.logMsg.reasonCode = output.reasonCode; - locals.logMsg._terminator = 0; + locals.logMsg._type = locals.result.executed ? QSBLogProposalExecuted : QSBLogProposalApproved; + locals.logMsg.proposalId = input.proposalId; + locals.logMsg.proposalType = locals.result.proposalType; + locals.logMsg.proposer = locals.result.proposer; + locals.logMsg.actor = qpi.invocator(); + locals.logMsg.approvalCount = locals.result.approvalCount; + locals.logMsg.success = output.success ? 1 : 0; + locals.logMsg.reasonCode = output.reasonCode; + locals.logMsg._terminator = 0; LOG_INFO(locals.logMsg); } struct CancelProposal_locals { - AdminProposal prop; + CancelProposalResult result; QSBLogProposalMessage logMsg; }; PUBLIC_PROCEDURE_WITH_LOCALS(CancelProposal) { - output.success = false; - output.reasonCode = QSBReasonNone; - - if (qpi.invocationReward() > 0) - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - - if (!isAdmin(state, qpi.invocator(), 0)) - { output.reasonCode = QSBReasonNotAdmin; return; } - - if (input.proposalId >= QSB_MAX_PROPOSALS) - { output.reasonCode = QSBReasonProposalNotFound; return; } - - locals.prop = state.get().proposals.get(input.proposalId); - - if (!locals.prop.active) - { output.reasonCode = QSBReasonProposalNotFound; return; } - - if (locals.prop.proposer != qpi.invocator()) - { output.reasonCode = QSBReasonNotProposer; return; } - - locals.prop.active = 0; - state.mut().proposals.set(input.proposalId, locals.prop); - output.success = true; + locals.result = tryCancelProposal(qpi, state, input); + output.success = locals.result.success; + output.reasonCode = locals.result.reasonCode; locals.logMsg._contractIndex = SELF_INDEX; - locals.logMsg._type = QSBLogProposalCancelled; - locals.logMsg.proposalId = input.proposalId; - locals.logMsg.proposalType = locals.prop.proposalType; - locals.logMsg.proposer = locals.prop.proposer; - locals.logMsg.actor = qpi.invocator(); - locals.logMsg.approvalCount = locals.prop.approvalCount; - locals.logMsg.success = 1; - locals.logMsg.reasonCode = QSBReasonNone; - locals.logMsg._terminator = 0; + locals.logMsg._type = QSBLogProposalCancelled; + locals.logMsg.proposalId = input.proposalId; + locals.logMsg.proposalType = locals.result.proposalType; + locals.logMsg.proposer = locals.result.proposer; + locals.logMsg.actor = qpi.invocator(); + locals.logMsg.approvalCount = locals.result.approvalCount; + locals.logMsg.success = output.success ? 1 : 0; + locals.logMsg.reasonCode = output.reasonCode; + locals.logMsg._terminator = 0; LOG_INFO(locals.logMsg); } struct Pause_locals { + PauseResult result; 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)) // Pause stays single-key (emergency brake) - { - 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.result = tryPause(qpi, state); + output.success = locals.result.success; locals.logMsg._contractIndex = SELF_INDEX; locals.logMsg._type = QSBLogPaused; locals.logMsg.caller = qpi.invocator(); - locals.logMsg.success = 1; - locals.logMsg.reasonCode = QSBReasonNone; + locals.logMsg.success = output.success ? 1 : 0; + locals.logMsg.reasonCode = locals.result.reasonCode; locals.logMsg._terminator = 0; LOG_INFO(locals.logMsg); } @@ -2083,25 +2202,11 @@ struct QSB : public ContractBase // Epoch processing // --------------------------------------------------------------------- - struct END_EPOCH_locals - { - uint32 i; - AdminProposal prop; - }; + struct END_EPOCH_locals {}; END_EPOCH_WITH_LOCALS() { - // Sweep expired proposals - for (locals.i = 0; locals.i < QSB_MAX_PROPOSALS; ++locals.i) - { - locals.prop = state.get().proposals.get(locals.i); - if (locals.prop.active && - qpi.epoch() > locals.prop.createdEpoch + QSB_PROPOSAL_EXPIRY_EPOCHS) - { - locals.prop.active = 0; - state.mut().proposals.set(locals.i, locals.prop); - } - } + sweepExpiredProposals(qpi, state); } // --------------------------------------------------------------------- @@ -2117,17 +2222,17 @@ struct QSB : public ContractBase setMemory(state.mut().admins, 0); state.mut().admins.set(0, id(100ULL, 200ULL, 300ULL, 400ULL)); state.mut().admins.set(1, id(101ULL, 201ULL, 301ULL, 401ULL)); - state.mut().adminCount = 2; + state.mut().adminCount = 2; state.mut().adminThreshold = 2; setMemory(state.mut().proposals, 0); state.mut().paused = false; - state.mut().oracleThreshold = 67; - state.mut().lastFilledOrdersNextOverwriteIdx = 0; - state.mut().lastLockedOrdersNextOverwriteIdx = 0; - state.mut().oracleCount = 0; - state.mut().pauserCount = 0; + state.mut().oracleThreshold = 67; + state.mut().lastFilledOrdersNextOverwriteIdx = 0; + state.mut().lastLockedOrdersNextOverwriteIdx = 0; + state.mut().oracleCount = 0; + state.mut().pauserCount = 0; setMemory(state.mut().oracles, 0); setMemory(state.mut().pausers, 0); @@ -2135,10 +2240,10 @@ struct QSB : public ContractBase setMemory(state.mut().filledOrdersPrev, 0); setMemory(state.mut().lockedOrders, 0); - state.mut().bpsFee = 0; - state.mut().protocolFee = 0; + state.mut().bpsFee = 0; + state.mut().protocolFee = 0; state.mut().protocolFeeRecipient = NULL_ID; - state.mut().oracleFeeRecipient = NULL_ID; + state.mut().oracleFeeRecipient = NULL_ID; state.mut().orderEra = 0; } diff --git a/test/contract_qsb.cpp b/test/contract_qsb.cpp index 5c872aed..52beca5d 100644 --- a/test/contract_qsb.cpp +++ b/test/contract_qsb.cpp @@ -93,9 +93,7 @@ class StateCheckerQSB : public QSB, public QSB::StateData // 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); + markOrderFilled(asMutState(), hash); } // Directly write an active locked order entry into a slot (bypasses contract call overhead). From 80ed7e26103ec005cca740e393f19ebcd42c77ad Mon Sep 17 00:00:00 2001 From: Jean Date: Wed, 10 Jun 2026 11:12:36 +0200 Subject: [PATCH 25/28] =?UTF-8?q?refactor(QSB):=20reorder=20sections=20?= =?UTF-8?q?=E2=80=94=20private=20helpers=20after=20public=20procedures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New layout: constants/public-types → protected result structs → public procedures/functions → protected helpers (low-level + try*) → epoch/init. Co-Authored-By: Claude Sonnet 4.6 --- src/contracts/QubicSolanaBridge.h | 2445 +++++++++++++++-------------- 1 file changed, 1224 insertions(+), 1221 deletions(-) diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h index ea530fb7..91f83621 100644 --- a/src/contracts/QubicSolanaBridge.h +++ b/src/contracts/QubicSolanaBridge.h @@ -661,1543 +661,1546 @@ struct QSB : public ContractBase protected: // --------------------------------------------------------------------- - // Low-level helpers + // Procedure result structs // --------------------------------------------------------------------- - inline static void digestToOrderHash(const id &digest, OrderHash &outHash) + struct LockResult { - outHash.setMem(digest); - } + bit success; + uint8 reasonCode; + OrderHash orderHash; + uint32 orderEra; + }; - inline static void initDomainPrefix(QSBOrderMessage &msg) + struct OverrideLockResult { - setMemory(msg, 0); - msg.protocolNameLen = 11; - msg.protocolName.set(0, 81); // Q - msg.protocolName.set(1, 117); // u - msg.protocolName.set(2, 98); // b - msg.protocolName.set(3, 105); // i - msg.protocolName.set(4, 99); // c - msg.protocolName.set(5, 66); // B - msg.protocolName.set(6, 114); // r - msg.protocolName.set(7, 105); // i - msg.protocolName.set(8, 100); // d - msg.protocolName.set(9, 103); // g - msg.protocolName.set(10, 101); // e - msg.protocolVersionLen = 1; - msg.protocolVersion.set(0, 49); // 1 - msg.contractAddress.set(0, (uint8)(CONTRACT_INDEX & 0xFF)); - msg.contractAddress.set(1, (uint8)((CONTRACT_INDEX >> 8) & 0xFF)); - } + bit success; + uint8 reasonCode; + OrderHash orderHash; + uint64 amount; + uint64 relayerFee; + uint32 networkOut; + uint32 orderEra; + }; - inline static void buildOrderMessage( - QSBOrderMessage &msg, - const Order &order, - OrderHash &tmpIdBytes) + struct UnlockResult { - uint32 i; - initDomainPrefix(msg); - msg.networkIn = order.networkIn; - msg.networkOut = order.networkOut; - for (i = 0; i < 32; ++i) - msg.tokenIn.set(i, order.tokenIn.get(i)); - for (i = 0; i < 32; ++i) - msg.tokenOut.set(i, order.tokenOut.get(i)); - tmpIdBytes.setMem(order.fromAddress); - for (i = 0; i < 32; ++i) - msg.fromAddress.set(i, tmpIdBytes.get(i)); - tmpIdBytes.setMem(order.toAddress); - for (i = 0; i < 32; ++i) - msg.toAddress.set(i, tmpIdBytes.get(i)); - msg.amount = order.amount; - msg.relayerFee = order.relayerFee; - for (i = 0; i < 32; ++i) - msg.nonce.set(i, order.nonce.get(i)); - msg.orderEra = order.orderEra; + bit success; + uint8 reasonCode; + OrderHash orderHash; + }; + + struct ProposeResult + { + bit success; + uint8 reasonCode; + uint8 proposalId; + }; + + struct ApproveProposalResult + { + bit success; + bit executed; + uint8 reasonCode; + uint8 proposalType; + id proposer; + uint8 approvalCount; + }; + + struct CancelProposalResult + { + bit success; + uint8 reasonCode; + uint8 proposalType; + id proposer; + uint8 approvalCount; + }; + + struct PauseResult + { + bit success; + uint8 reasonCode; + }; + +public: + // --------------------------------------------------------------------- + // Core user procedures + // --------------------------------------------------------------------- + + struct Lock_locals + { + LockResult result; + QSBLogLockMessage logMsg; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(Lock) + { + locals.result = tryLock(qpi, state, input); + output.success = locals.result.success; + output.orderHash = locals.result.orderHash; + + 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; + locals.logMsg.orderHash = locals.result.orderHash; + locals.logMsg.success = locals.result.success ? 1 : 0; + locals.logMsg.reasonCode = locals.result.reasonCode; + locals.logMsg.orderEra = locals.result.orderEra; + locals.logMsg._terminator = 0; + LOG_INFO(locals.logMsg); } - inline static uint8 countBitsUint8(uint8 mask) + struct OverrideLock_locals { - uint8 count = 0; - for (uint8 i = 0; i < 8; ++i) - { - if (mask & (uint8)(1u << i)) - ++count; - } - return count; + OverrideLockResult result; + QSBLogOverrideLockMessage logMsg; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(OverrideLock) + { + locals.result = tryOverrideLock(qpi, state, input); + output.success = locals.result.success; + output.orderHash = locals.result.orderHash; + + locals.logMsg._contractIndex = SELF_INDEX; + locals.logMsg._type = QSBLogOverrideLock; + locals.logMsg.from = qpi.invocator(); + setMemory(locals.logMsg.to, 0); + if (locals.result.success) + copyFromBuffer(locals.logMsg.to, input.toAddress); + locals.logMsg.amount = locals.result.amount; + locals.logMsg.relayerFee = locals.result.relayerFee; + locals.logMsg.networkOut = locals.result.networkOut; + locals.logMsg.nonce = input.nonce; + locals.logMsg.orderHash = locals.result.orderHash; + locals.logMsg.success = locals.result.success ? 1 : 0; + locals.logMsg.reasonCode = locals.result.reasonCode; + locals.logMsg.orderEra = locals.result.orderEra; + locals.logMsg._terminator = 0; + LOG_INFO(locals.logMsg); } - inline static bool isAdmin(const QPI::ContractState &state, const id &who) + struct Unlock_locals { - for (uint32 i = 0; i < QSB_MAX_ADMINS; ++i) - { - if (!isZero(state.get().admins.get(i)) && state.get().admins.get(i) == who) - return true; - } - return false; + UnlockResult result; + QSBLogUnlockMessage logMsg; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(Unlock) + { + locals.result = tryUnlock(qpi, state, input); + output.success = locals.result.success; + output.orderHash = locals.result.orderHash; + + locals.logMsg._contractIndex = SELF_INDEX; + locals.logMsg._type = QSBLogUnlock; + locals.logMsg.orderHash = locals.result.orderHash; + 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.success = locals.result.success ? 1 : 0; + locals.logMsg.reasonCode = locals.result.reasonCode; + locals.logMsg.orderEra = input.order.orderEra; + locals.logMsg._terminator = 0; + LOG_INFO(locals.logMsg); } - inline static sint64 findAdminIndex(const QPI::ContractState &state, const id &who) + // View functions + PUBLIC_FUNCTION(GetConfig) { - for (uint32 i = 0; i < QSB_MAX_ADMINS; ++i) - { - if (!isZero(state.get().admins.get(i)) && state.get().admins.get(i) == who) - return (sint64)i; - } - return NULL_INDEX; + output.adminCount = state.get().adminCount; + output.adminThreshold = state.get().adminThreshold; + output.admins = state.get().admins; + 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; } - inline static bool isAdminOrPauser(const QPI::ContractState &state, const id &who) + PUBLIC_FUNCTION(GetProposal) { - if (isAdmin(state, who)) - return true; - for (uint32 i = 0; i < state.get().pausers.capacity(); ++i) + output.exists = false; + if (input.proposalId < QSB_MAX_PROPOSALS) { - if (state.get().pausers.get(i).active && state.get().pausers.get(i).account == who) - return true; + output.proposal = state.get().proposals.get(input.proposalId); + output.exists = output.proposal.active; } - return false; } - // Cancel all pending proposals — called when the admin set changes to invalidate stale votes. - inline static void cancelAllPendingProposals(QPI::ContractState &state) + struct GetProposals_locals { + uint32 i; AdminProposal prop; - for (uint32 i = 0; i < QSB_MAX_PROPOSALS; ++i) + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetProposals) + { + output.count = 0; + setMemory(output.proposals, 0); + for (locals.i = 0; locals.i < QSB_MAX_PROPOSALS; ++locals.i) { - prop = state.get().proposals.get(i); - if (prop.active) + locals.prop = state.get().proposals.get(locals.i); + if (locals.prop.active) { - prop.active = 0; - state.mut().proposals.set(i, prop); + output.proposals.set(output.count, locals.prop); + ++output.count; } } } - // Pure state mutation — no qpi access. - inline static bool executeProposalPayload(QPI::ContractState &state, const AdminProposal &prop) + PUBLIC_FUNCTION(IsOracle) { - uint32 i; - RoleEntry entry; - sint64 idx; + output.isOracle = (findOracleIndex(state, input.account) != NULL_INDEX); + } - if (prop.proposalType == QSBPropAddAdmin) - { - for (i = 0; i < QSB_MAX_ADMINS; ++i) - { - if (isZero(state.get().admins.get(i))) - { - state.mut().admins.set(i, prop.targetId); - state.mut().adminCount = state.get().adminCount + 1; - return true; - } - } - return false; - } - else if (prop.proposalType == QSBPropRemoveAdmin) - { - idx = findAdminIndex(state, prop.targetId); - state.mut().admins.set((uint32)idx, NULL_ID); - state.mut().adminCount = state.get().adminCount - 1; - return true; - } - else if (prop.proposalType == QSBPropSetAdminThreshold) - { - state.mut().adminThreshold = prop.newAdminThreshold; - return true; - } - else if (prop.proposalType == QSBPropAddRole) - { - if (prop.role == (uint8)Role::Oracle) - { - if (findOracleIndex(state, prop.targetId) != NULL_INDEX) - return true; - for (i = 0; i < state.get().oracles.capacity(); ++i) - { - entry = state.get().oracles.get(i); - if (!entry.active) - { - entry.account = prop.targetId; - entry.active = true; - state.mut().oracles.set(i, entry); - ++state.mut().oracleCount; - return true; - } - } - return false; - } - else if (prop.role == (uint8)Role::Pauser) - { - if (findPauserIndex(state, prop.targetId) != NULL_INDEX) - return true; - for (i = 0; i < state.get().pausers.capacity(); ++i) - { - entry = state.get().pausers.get(i); - if (!entry.active) - { - entry.account = prop.targetId; - entry.active = true; - state.mut().pausers.set(i, entry); - ++state.mut().pauserCount; - return true; - } - } - return false; - } - return false; - } - else if (prop.proposalType == QSBPropRemoveRole) - { - if (prop.role == (uint8)Role::Oracle) - { - idx = findOracleIndex(state, prop.targetId); - if (idx == NULL_INDEX) - return true; - entry = state.get().oracles.get((uint32)idx); - entry.active = false; - state.mut().oracles.set((uint32)idx, entry); - if (state.get().oracleCount > 0) - --state.mut().oracleCount; - return true; - } - else if (prop.role == (uint8)Role::Pauser) - { - idx = findPauserIndex(state, prop.targetId); - if (idx == NULL_INDEX) - return true; - entry = state.get().pausers.get((uint32)idx); - entry.active = false; - state.mut().pausers.set((uint32)idx, entry); - if (state.get().pauserCount > 0) - --state.mut().pauserCount; - return true; - } - return false; - } - else if (prop.proposalType == QSBPropEditOracleThreshold) - { - state.mut().oracleThreshold = prop.newOracleThreshold; - return true; - } - else if (prop.proposalType == QSBPropEditFeeParameters) - { - if (prop.bpsFee != 0 && prop.bpsFee <= QSB_MAX_BPS_FEE) - state.mut().bpsFee = prop.bpsFee; - if (prop.protocolFee != 0 && prop.protocolFee <= QSB_MAX_PROTOCOL_FEE) - state.mut().protocolFee = prop.protocolFee; - if (!isZero(prop.protocolFeeRecipient)) - state.mut().protocolFeeRecipient = prop.protocolFeeRecipient; - if (!isZero(prop.oracleFeeRecipient)) - state.mut().oracleFeeRecipient = prop.oracleFeeRecipient; - return true; - } - else if (prop.proposalType == QSBPropUnpause) - { - state.mut().paused = false; - return true; - } - return false; + PUBLIC_FUNCTION(IsPauser) + { + output.isPauser = (findPauserIndex(state, input.account) != NULL_INDEX); } - inline static sint64 findOracleIndex(const QPI::ContractState &state, const id &account) + struct GetLockedOrder_locals { - for (uint32 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; - } + sint64 idx; + }; - inline static sint64 findPauserIndex(const QPI::ContractState &state, const id &account) + PUBLIC_FUNCTION_WITH_LOCALS(GetLockedOrder) { - for (uint32 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; + locals.idx = findLockedOrderIndexByNonce(state, input.nonce); + output.exists = (locals.idx != NULL_INDEX); + if (output.exists) + output.order = state.get().lockedOrders.get((uint32)locals.idx); } - // Idempotent insert into ring-buffer filled-order storage. - inline static void markOrderFilled(QPI::ContractState &state, const OrderHash &hash) + PUBLIC_FUNCTION(IsOrderFilled) { - uint32 i, j; - bool same; - FilledOrderEntry entry; + output.filled = isOrderFilled(state, input.hash); + } - 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; - } - } + struct ComputeOrderHash_locals + { + id digest; + QSBOrderMessage msgBuffer; + OrderHash tmpIdBytes; + }; - 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) - { - // On ring buffer wrap: preserve current buffer as prev, clear current, advance era. - state.mut().filledOrdersPrev = state.get().filledOrders; - setMemory(state.mut().filledOrders, 0); - state.mut().orderEra = state.get().orderEra + 1; - } + PUBLIC_FUNCTION_WITH_LOCALS(ComputeOrderHash) + { + buildOrderMessage(locals.msgBuffer, input.order, locals.tmpIdBytes); + locals.digest = qpi.K12(locals.msgBuffer); + output.hash.setMem(locals.digest); } - // Checks current and previous era buffers to cover in-flight orders across a ring wrap. - inline static bit isOrderFilled(const QPI::ContractState &state, const OrderHash &hash) + struct GetOracles_locals { - uint32 i, j; - bool same; - FilledOrderEntry entry; + uint32 i; + RoleEntry entry; + }; - for (i = 0; i < state.get().filledOrders.capacity(); ++i) + 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) { - entry = state.get().filledOrders.get(i); - if (!entry.used) - continue; - same = true; - for (j = 0; j < hash.capacity(); ++j) + locals.entry = state.get().oracles.get(locals.i); + if (locals.entry.active) { - if (entry.hash.get(j) != hash.get(j)) - { - same = false; - break; - } + output.accounts.set(output.count, locals.entry.account); + ++output.count; } - if (same) - return true; } - for (i = 0; i < state.get().filledOrdersPrev.capacity(); ++i) + } + + 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) { - entry = state.get().filledOrdersPrev.get(i); - if (!entry.used) - continue; - same = true; - for (j = 0; j < hash.capacity(); ++j) + locals.entry = state.get().pausers.get(locals.i); + if (locals.entry.active) { - if (entry.hash.get(j) != hash.get(j)) - { - same = false; - break; - } + output.accounts.set(output.count, locals.entry.account); + ++output.count; } - if (same) - return true; } - return false; } - inline static sint64 findLockedOrderIndexByNonce(const QPI::ContractState &state, uint32 nonce) + struct GetLockedOrders_locals { - for (uint32 i = 0; i < QSB_MAX_LOCKED_ORDERS; ++i) + uint32 i; + uint32 slot; + 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; + // Iterate most-recent-first: start one slot before the next write position + for (locals.i = 0; locals.i < QSB_MAX_LOCKED_ORDERS; ++locals.i) { - if (state.get().lockedOrders.get(i).active && state.get().lockedOrders.get(i).nonce == nonce) - return (sint32)i; + locals.slot = (state.get().lastLockedOrdersNextOverwriteIdx + QSB_MAX_LOCKED_ORDERS - 1 - locals.i) & (QSB_MAX_LOCKED_ORDERS - 1); + locals.entry = state.get().lockedOrders.get(locals.slot); + 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; } - return NULL_INDEX; + output.totalActive = locals.totalActive; + output.returned = locals.collected; } - // Sweep expired proposals — called from END_EPOCH. - inline static void sweepExpiredProposals( - const QPI::QpiContextProcedureCall &qpi, - QPI::ContractState &state) + struct GetFilledOrders_locals { - AdminProposal prop; - for (uint32 i = 0; i < QSB_MAX_PROPOSALS; ++i) + uint32 i; + uint32 slot; + 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; + // Iterate most-recent-first: start one slot before the next write position + for (locals.i = 0; locals.i < QSB_MAX_FILLED_ORDERS; ++locals.i) { - prop = state.get().proposals.get(i); - if (prop.active && qpi.epoch() > prop.createdEpoch + QSB_PROPOSAL_EXPIRY_EPOCHS) - { - prop.active = 0; - state.mut().proposals.set(i, prop); - } + locals.slot = (state.get().lastFilledOrdersNextOverwriteIdx + QSB_MAX_FILLED_ORDERS - 1 - locals.i) & (QSB_MAX_FILLED_ORDERS - 1); + locals.entry = state.get().filledOrders.get(locals.slot); + 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; } // --------------------------------------------------------------------- - // Procedure result structs + // Admin procedures (multisig) // --------------------------------------------------------------------- - struct LockResult + struct Propose_locals { - bit success; - uint8 reasonCode; - OrderHash orderHash; - uint32 orderEra; + ProposeResult result; + QSBLogProposalMessage logMsg; }; - struct OverrideLockResult + PUBLIC_PROCEDURE_WITH_LOCALS(Propose) { - bit success; - uint8 reasonCode; - OrderHash orderHash; - uint64 amount; - uint64 relayerFee; - uint32 networkOut; - uint32 orderEra; - }; + locals.result = tryPropose(qpi, state, input); + output.success = locals.result.success; + output.reasonCode = locals.result.reasonCode; + output.proposalId = locals.result.proposalId; - struct UnlockResult - { - bit success; - uint8 reasonCode; - OrderHash orderHash; - }; + locals.logMsg._contractIndex = SELF_INDEX; + locals.logMsg._type = QSBLogProposalCreated; + locals.logMsg.proposalId = output.proposalId; + locals.logMsg.proposalType = input.proposalType; + locals.logMsg.proposer = qpi.invocator(); + locals.logMsg.actor = qpi.invocator(); + locals.logMsg.approvalCount = 1; + locals.logMsg.success = output.success ? 1 : 0; + locals.logMsg.reasonCode = output.reasonCode; + locals.logMsg._terminator = 0; + LOG_INFO(locals.logMsg); + } - struct ProposeResult + struct ApproveProposal_locals { - bit success; - uint8 reasonCode; - uint8 proposalId; + ApproveProposalResult result; + QSBLogProposalMessage logMsg; }; - struct ApproveProposalResult + PUBLIC_PROCEDURE_WITH_LOCALS(ApproveProposal) { - bit success; - bit executed; - uint8 reasonCode; - uint8 proposalType; - id proposer; - uint8 approvalCount; - }; + locals.result = tryApproveProposal(qpi, state, input); + output.success = locals.result.success; + output.executed = locals.result.executed; + output.reasonCode = locals.result.reasonCode; - struct CancelProposalResult - { - bit success; - uint8 reasonCode; - uint8 proposalType; - id proposer; - uint8 approvalCount; - }; + locals.logMsg._contractIndex = SELF_INDEX; + locals.logMsg._type = locals.result.executed ? QSBLogProposalExecuted : QSBLogProposalApproved; + locals.logMsg.proposalId = input.proposalId; + locals.logMsg.proposalType = locals.result.proposalType; + locals.logMsg.proposer = locals.result.proposer; + locals.logMsg.actor = qpi.invocator(); + locals.logMsg.approvalCount = locals.result.approvalCount; + locals.logMsg.success = output.success ? 1 : 0; + locals.logMsg.reasonCode = output.reasonCode; + locals.logMsg._terminator = 0; + LOG_INFO(locals.logMsg); + } - struct PauseResult + struct CancelProposal_locals { - bit success; - uint8 reasonCode; + CancelProposalResult result; + QSBLogProposalMessage logMsg; }; - // --------------------------------------------------------------------- - // Procedure logic helpers (try*) - // All logic lives here; LOG_INFO stays in the thin procedure shell. - // --------------------------------------------------------------------- - - inline static LockResult tryLock( - const QPI::QpiContextProcedureCall &qpi, - QPI::ContractState &state, - const Lock_input &input) + PUBLIC_PROCEDURE_WITH_LOCALS(CancelProposal) { - LockResult result = { false, QSBReasonNone, {}, 0 }; - - if (state.get().paused) - { - if (qpi.invocationReward() > 0) - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - result.reasonCode = QSBReasonPaused; - return result; - } - - if (input.amount == 0 || input.relayerFee >= input.amount) - { - if (qpi.invocationReward() > 0) - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - result.reasonCode = QSBReasonInvalidAmount; - return result; - } - - if (qpi.invocationReward() < (sint64)input.amount) - { - if (qpi.invocationReward() > 0) - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - result.reasonCode = QSBReasonInsufficientReward; - return result; - } - - // Any excess over `amount` is refunded; exactly `amount` stays locked. - if (qpi.invocationReward() > (sint64)input.amount) - qpi.transfer(qpi.invocator(), qpi.invocationReward() - input.amount); - - if (findLockedOrderIndexByNonce(state, input.nonce) != NULL_INDEX) - { - qpi.transfer(qpi.invocator(), input.amount); - result.reasonCode = QSBReasonNonceUsed; - return result; - } - - Order tmpOrder; - tmpOrder.networkIn = 1; - tmpOrder.networkOut = input.networkOut; - setMemory(tmpOrder.tokenIn, 0); - setMemory(tmpOrder.tokenOut, 0); - tmpOrder.fromAddress = qpi.invocator(); - tmpOrder.toAddress = NULL_ID; - tmpOrder.amount = input.amount; - tmpOrder.relayerFee = input.relayerFee; - setMemory(tmpOrder.nonce, 0); - tmpOrder.nonce.set(0, (uint8)(input.nonce & 0xFF)); - tmpOrder.nonce.set(1, (uint8)((input.nonce >> 8) & 0xFF)); - tmpOrder.nonce.set(2, (uint8)((input.nonce >> 16) & 0xFF)); - tmpOrder.nonce.set(3, (uint8)((input.nonce >> 24) & 0xFF)); - tmpOrder.orderEra = state.get().orderEra; - - QSBOrderMessage msgBuffer; - OrderHash tmpIdBytes; - buildOrderMessage(msgBuffer, tmpOrder, tmpIdBytes); - id digest = qpi.K12(msgBuffer); - digestToOrderHash(digest, result.orderHash); - result.orderEra = state.get().orderEra; - - // Persist in ring buffer; oldest slot overwritten when full. - // By the time the ring wraps (1024 orders), off-chain tooling has indexed earlier entries. - LockedOrderEntry entry; - entry.active = true; - entry.sender = qpi.invocator(); - entry.networkOut = input.networkOut; - entry.amount = input.amount; - entry.relayerFee = input.relayerFee; - entry.nonce = input.nonce; - copyMemory(entry.toAddress, input.toAddress); - entry.orderHash = result.orderHash; - entry.lockEpoch = qpi.epoch(); - entry.orderEra = state.get().orderEra; - entry.overrideLockCount = 0; - state.mut().lockedOrders.set(state.get().lastLockedOrdersNextOverwriteIdx, entry); - state.mut().lastLockedOrdersNextOverwriteIdx = - (state.get().lastLockedOrdersNextOverwriteIdx + 1) & (QSB_MAX_LOCKED_ORDERS - 1); + locals.result = tryCancelProposal(qpi, state, input); + output.success = locals.result.success; + output.reasonCode = locals.result.reasonCode; - result.success = true; - return result; + locals.logMsg._contractIndex = SELF_INDEX; + locals.logMsg._type = QSBLogProposalCancelled; + locals.logMsg.proposalId = input.proposalId; + locals.logMsg.proposalType = locals.result.proposalType; + locals.logMsg.proposer = locals.result.proposer; + locals.logMsg.actor = qpi.invocator(); + locals.logMsg.approvalCount = locals.result.approvalCount; + locals.logMsg.success = output.success ? 1 : 0; + locals.logMsg.reasonCode = output.reasonCode; + locals.logMsg._terminator = 0; + LOG_INFO(locals.logMsg); } - inline static OverrideLockResult tryOverrideLock( - const QPI::QpiContextProcedureCall &qpi, - QPI::ContractState &state, - const OverrideLock_input &input) + struct Pause_locals { - OverrideLockResult result = { false, QSBReasonNone, {}, 0, 0, 0, 0 }; - - // Always refund — locking was done in the original lock() call. - if (qpi.invocationReward() > 0) - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - - if (state.get().paused) - { - result.reasonCode = QSBReasonPaused; - return result; - } - - sint64 idx = findLockedOrderIndexByNonce(state, input.nonce); - if (idx == NULL_INDEX) - { - result.reasonCode = QSBReasonOrderNotFound; - return result; - } + PauseResult result; + QSBLogPausedMessage logMsg; + }; - LockedOrderEntry entry = state.get().lockedOrders.get((uint32)idx); + PUBLIC_PROCEDURE_WITH_LOCALS(Pause) + { + locals.result = tryPause(qpi, state); + output.success = locals.result.success; - if (entry.sender != qpi.invocator()) - { - result.reasonCode = QSBReasonNotSender; - return result; - } + locals.logMsg._contractIndex = SELF_INDEX; + locals.logMsg._type = QSBLogPaused; + locals.logMsg.caller = qpi.invocator(); + locals.logMsg.success = output.success ? 1 : 0; + locals.logMsg.reasonCode = locals.result.reasonCode; + locals.logMsg._terminator = 0; + LOG_INFO(locals.logMsg); + } - if (entry.overrideLockCount >= QSB_OVERRIDE_LOCK_MAX_ATTEMPTS) - { - result.reasonCode = QSBReasonOverrideLimitReached; - return result; - } + 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); + REGISTER_USER_FUNCTION(GetProposal, 11); + REGISTER_USER_FUNCTION(GetProposals, 12); - if (input.relayerFee >= entry.amount) - { - result.reasonCode = QSBReasonBadRelayerFee; - return result; - } + // User procedures + REGISTER_USER_PROCEDURE(Lock, 1); + REGISTER_USER_PROCEDURE(OverrideLock, 2); + REGISTER_USER_PROCEDURE(Unlock, 3); - copyMemory(entry.toAddress, input.toAddress); - entry.relayerFee = input.relayerFee; + // Emergency pause — single-key, any admin or pauser + REGISTER_USER_PROCEDURE(Pause, 14); - Order tmpOrder; - tmpOrder.networkIn = 1; - tmpOrder.networkOut = entry.networkOut; - setMemory(tmpOrder.tokenIn, 0); - setMemory(tmpOrder.tokenOut, 0); - tmpOrder.fromAddress = entry.sender; - tmpOrder.toAddress = NULL_ID; - tmpOrder.amount = entry.amount; - tmpOrder.relayerFee = entry.relayerFee; - setMemory(tmpOrder.nonce, 0); - tmpOrder.nonce.set(0, (uint8)(entry.nonce & 0xFF)); - tmpOrder.nonce.set(1, (uint8)((entry.nonce >> 8) & 0xFF)); - tmpOrder.nonce.set(2, (uint8)((entry.nonce >> 16) & 0xFF)); - tmpOrder.nonce.set(3, (uint8)((entry.nonce >> 24) & 0xFF)); - tmpOrder.orderEra = entry.orderEra; // preserve original era + // Multisig admin procedures + REGISTER_USER_PROCEDURE(Propose, 20); + REGISTER_USER_PROCEDURE(ApproveProposal, 21); + REGISTER_USER_PROCEDURE(CancelProposal, 22); + } - QSBOrderMessage msgBuffer; - OrderHash tmpIdBytes; - buildOrderMessage(msgBuffer, tmpOrder, tmpIdBytes); - id digest = qpi.K12(msgBuffer); - digestToOrderHash(digest, entry.orderHash); +protected: + // --------------------------------------------------------------------- + // Low-level helpers + // --------------------------------------------------------------------- - entry.overrideLockCount++; - state.mut().lockedOrders.set((uint32)idx, entry); + inline static void digestToOrderHash(const id &digest, OrderHash &outHash) + { + outHash.setMem(digest); + } - result.orderHash = entry.orderHash; - result.amount = entry.amount; - result.relayerFee = entry.relayerFee; - result.networkOut = entry.networkOut; - result.orderEra = entry.orderEra; - result.success = true; - return result; + inline static void initDomainPrefix(QSBOrderMessage &msg) + { + setMemory(msg, 0); + msg.protocolNameLen = 11; + msg.protocolName.set(0, 81); // Q + msg.protocolName.set(1, 117); // u + msg.protocolName.set(2, 98); // b + msg.protocolName.set(3, 105); // i + msg.protocolName.set(4, 99); // c + msg.protocolName.set(5, 66); // B + msg.protocolName.set(6, 114); // r + msg.protocolName.set(7, 105); // i + msg.protocolName.set(8, 100); // d + msg.protocolName.set(9, 103); // g + msg.protocolName.set(10, 101); // e + msg.protocolVersionLen = 1; + msg.protocolVersion.set(0, 49); // 1 + msg.contractAddress.set(0, (uint8)(CONTRACT_INDEX & 0xFF)); + msg.contractAddress.set(1, (uint8)((CONTRACT_INDEX >> 8) & 0xFF)); } - inline static UnlockResult tryUnlock( - const QPI::QpiContextProcedureCall &qpi, - QPI::ContractState &state, - const Unlock_input &input) + inline static void buildOrderMessage( + QSBOrderMessage &msg, + const Order &order, + OrderHash &tmpIdBytes) { - UnlockResult result = { false, QSBReasonNone, {} }; - - if (state.get().paused) - { - if (qpi.invocationReward() > 0) - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - result.reasonCode = QSBReasonPaused; - return result; - } - - // Refund invocation reward — relayer is paid from order.amount, not from reward. - if (qpi.invocationReward() > 0) - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + uint32 i; + initDomainPrefix(msg); + msg.networkIn = order.networkIn; + msg.networkOut = order.networkOut; + for (i = 0; i < 32; ++i) + msg.tokenIn.set(i, order.tokenIn.get(i)); + for (i = 0; i < 32; ++i) + msg.tokenOut.set(i, order.tokenOut.get(i)); + tmpIdBytes.setMem(order.fromAddress); + for (i = 0; i < 32; ++i) + msg.fromAddress.set(i, tmpIdBytes.get(i)); + tmpIdBytes.setMem(order.toAddress); + for (i = 0; i < 32; ++i) + msg.toAddress.set(i, tmpIdBytes.get(i)); + msg.amount = order.amount; + msg.relayerFee = order.relayerFee; + for (i = 0; i < 32; ++i) + msg.nonce.set(i, order.nonce.get(i)); + msg.orderEra = order.orderEra; + } - if (input.order.amount == 0 || input.order.relayerFee >= input.order.amount) + inline static uint8 countBitsUint8(uint8 mask) + { + uint8 count = 0; + for (uint8 i = 0; i < 8; ++i) { - result.reasonCode = QSBReasonInvalidAmount; - return result; + if (mask & (uint8)(1u << i)) + ++count; } + return count; + } - // Defensive balance check — should never fail under normal operation since - // Lock keeps funds inside the contract, but guards against unexpected discrepancies. - Entity entity; - qpi.getEntity(SELF, entity); - uint64 contractBalance = (entity.incomingAmount >= entity.outgoingAmount) - ? entity.incomingAmount - entity.outgoingAmount - : 0; - - if (contractBalance < input.order.amount) + inline static bool isAdmin(const QPI::ContractState &state, const id &who) + { + for (uint32 i = 0; i < QSB_MAX_ADMINS; ++i) { - result.reasonCode = QSBReasonInsufficientReward; - return result; + if (!isZero(state.get().admins.get(i)) && state.get().admins.get(i) == who) + return true; } + return false; + } - // Accept current era or immediately previous era. - // The N-1 grace window covers in-flight orders signed just before a ring-buffer wrap; - // isOrderFilled checks both buffers to prevent replays across the boundary. - if (input.order.orderEra != state.get().orderEra && - !(state.get().orderEra > 0 && input.order.orderEra == state.get().orderEra - 1)) + inline static sint64 findAdminIndex(const QPI::ContractState &state, const id &who) + { + for (uint32 i = 0; i < QSB_MAX_ADMINS; ++i) { - result.reasonCode = QSBReasonEraMismatch; - return result; + if (!isZero(state.get().admins.get(i)) && state.get().admins.get(i) == who) + return (sint64)i; } + return NULL_INDEX; + } - // 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 a balance check. - // This models a fungible lock/mint ↔ burn/unlock bridge where minted tokens - // can be freely transferred and aggregated. - - QSBOrderMessage msgBuffer; - OrderHash tmpIdBytes; - buildOrderMessage(msgBuffer, input.order, tmpIdBytes); - id digest = qpi.K12(msgBuffer); - digestToOrderHash(digest, result.orderHash); - - FilledOrderEntry entry; - if (isOrderFilled(state, result.orderHash)) + inline static bool isAdminOrPauser(const QPI::ContractState &state, const id &who) + { + if (isAdmin(state, who)) + return true; + for (uint32 i = 0; i < state.get().pausers.capacity(); ++i) { - result.reasonCode = QSBReasonAlreadyFilled; - return result; + if (state.get().pausers.get(i).active && state.get().pausers.get(i).account == who) + return true; } + return false; + } - if (state.get().oracleCount == 0 || input.numSignatures == 0) + // Cancel all pending proposals — called when the admin set changes to invalidate stale votes. + inline static void cancelAllPendingProposals(QPI::ContractState &state) + { + AdminProposal prop; + for (uint32 i = 0; i < QSB_MAX_PROPOSALS; ++i) { - result.reasonCode = QSBReasonNoOracles; - return result; + prop = state.get().proposals.get(i); + if (prop.active) + { + prop.active = 0; + state.mut().proposals.set(i, prop); + } } + } - // requiredSignatures = ceil(oracleCount * oracleThreshold / 100) - uint128 tmpMul = uint128(state.get().oracleCount) * uint128(state.get().oracleThreshold); - uint128 tmpMul2 = div(tmpMul, uint128(100)); - uint32 requiredSignatures = (uint32)tmpMul2.low; - if (requiredSignatures * 100 < state.get().oracleCount * state.get().oracleThreshold) - ++requiredSignatures; - if (requiredSignatures == 0) - requiredSignatures = 1; - - uint32 validSignatureCount = 0; - uint32 seenCount = 0; - Array seenSigners; + // Pure state mutation — no qpi access. + inline static bool executeProposalPayload(QPI::ContractState &state, const AdminProposal &prop) + { + uint32 i; + RoleEntry entry; + sint64 idx; - for (uint32 i = 0; i < input.numSignatures && i < input.signatures.capacity(); ++i) + if (prop.proposalType == QSBPropAddAdmin) { - SignatureData sig = input.signatures.get(i); - - if (findOracleIndex(state, sig.signer) == NULL_INDEX) - { - result.reasonCode = QSBReasonInvalidSignature; - return result; - } - - for (uint32 j = 0; j < seenCount; ++j) + for (i = 0; i < QSB_MAX_ADMINS; ++i) { - if (seenSigners.get(j) == sig.signer) + if (isZero(state.get().admins.get(i))) { - result.reasonCode = QSBReasonDuplicateSigner; - return result; + state.mut().admins.set(i, prop.targetId); + state.mut().adminCount = state.get().adminCount + 1; + return true; } } - - if (!qpi.signatureValidity(sig.signer, digest, sig.signature)) - { - result.reasonCode = QSBReasonInvalidSignature; - return result; - } - - if (seenCount < seenSigners.capacity()) - { - seenSigners.set(seenCount, sig.signer); - ++seenCount; - } - ++validSignatureCount; + return false; } - - if (validSignatureCount < requiredSignatures) + else if (prop.proposalType == QSBPropRemoveAdmin) { - result.reasonCode = QSBReasonThresholdFailed; - return result; + idx = findAdminIndex(state, prop.targetId); + state.mut().admins.set((uint32)idx, NULL_ID); + state.mut().adminCount = state.get().adminCount - 1; + return true; } - - // bpsFeeAmount = netAmount * bpsFee / 10000 - uint64 netAmount = input.order.amount - input.order.relayerFee; - tmpMul = uint128(netAmount) * uint128(state.get().bpsFee); - tmpMul2 = div(tmpMul, uint128(10000)); - uint64 bpsFeeAmount = (uint64)tmpMul2.low; - - // protocolFeeAmount = bpsFeeAmount * protocolFee / 100 - tmpMul = uint128(bpsFeeAmount) * uint128(state.get().protocolFee); - tmpMul2 = div(tmpMul, uint128(100)); - uint64 protocolFeeAmount = (uint64)tmpMul2.low; - - // oracleFeeAmount = bpsFeeAmount - protocolFeeAmount - uint64 oracleFeeAmount = (bpsFeeAmount >= protocolFeeAmount) ? bpsFeeAmount - protocolFeeAmount : 0; - - // recipientAmount = netAmount - bpsFeeAmount - uint64 recipientAmount = (netAmount >= bpsFeeAmount) ? netAmount - bpsFeeAmount : 0; - - // Mark filled BEFORE transfers to prevent replay on partial transfer failure. - // The balance check above guarantees the contract has enough funds. - markOrderFilled(state, result.orderHash); - - bool allTransfersOk = true; - - // Recipient payout first (most important transfer) - if (recipientAmount > 0 && !isZero(input.order.toAddress)) + else if (prop.proposalType == QSBPropSetAdminThreshold) { - if (qpi.transfer(input.order.toAddress, (sint64)recipientAmount) < 0) - allTransfersOk = false; + state.mut().adminThreshold = prop.newAdminThreshold; + return true; + } + else if (prop.proposalType == QSBPropAddRole) + { + if (prop.role == (uint8)Role::Oracle) + { + if (findOracleIndex(state, prop.targetId) != NULL_INDEX) + return true; + for (i = 0; i < state.get().oracles.capacity(); ++i) + { + entry = state.get().oracles.get(i); + if (!entry.active) + { + entry.account = prop.targetId; + entry.active = true; + state.mut().oracles.set(i, entry); + ++state.mut().oracleCount; + return true; + } + } + return false; + } + else if (prop.role == (uint8)Role::Pauser) + { + if (findPauserIndex(state, prop.targetId) != NULL_INDEX) + return true; + for (i = 0; i < state.get().pausers.capacity(); ++i) + { + entry = state.get().pausers.get(i); + if (!entry.active) + { + entry.account = prop.targetId; + entry.active = true; + state.mut().pausers.set(i, entry); + ++state.mut().pauserCount; + return true; + } + } + return false; + } + return false; } - - if (input.order.relayerFee > 0) + else if (prop.proposalType == QSBPropRemoveRole) { - if (qpi.transfer(qpi.invocator(), (sint64)input.order.relayerFee) < 0) - allTransfersOk = false; + if (prop.role == (uint8)Role::Oracle) + { + idx = findOracleIndex(state, prop.targetId); + if (idx == NULL_INDEX) + return true; + entry = state.get().oracles.get((uint32)idx); + entry.active = false; + state.mut().oracles.set((uint32)idx, entry); + if (state.get().oracleCount > 0) + --state.mut().oracleCount; + return true; + } + else if (prop.role == (uint8)Role::Pauser) + { + idx = findPauserIndex(state, prop.targetId); + if (idx == NULL_INDEX) + return true; + entry = state.get().pausers.get((uint32)idx); + entry.active = false; + state.mut().pausers.set((uint32)idx, entry); + if (state.get().pauserCount > 0) + --state.mut().pauserCount; + return true; + } + return false; } - - if (protocolFeeAmount > 0 && !isZero(state.get().protocolFeeRecipient)) + else if (prop.proposalType == QSBPropEditOracleThreshold) { - if (qpi.transfer(state.get().protocolFeeRecipient, (sint64)protocolFeeAmount) < 0) - allTransfersOk = false; + state.mut().oracleThreshold = prop.newOracleThreshold; + return true; } - - if (oracleFeeAmount > 0 && !isZero(state.get().oracleFeeRecipient)) + else if (prop.proposalType == QSBPropEditFeeParameters) { - if (qpi.transfer(state.get().oracleFeeRecipient, (sint64)oracleFeeAmount) < 0) - allTransfersOk = false; + if (prop.bpsFee != 0 && prop.bpsFee <= QSB_MAX_BPS_FEE) + state.mut().bpsFee = prop.bpsFee; + if (prop.protocolFee != 0 && prop.protocolFee <= QSB_MAX_PROTOCOL_FEE) + state.mut().protocolFee = prop.protocolFee; + if (!isZero(prop.protocolFeeRecipient)) + state.mut().protocolFeeRecipient = prop.protocolFeeRecipient; + if (!isZero(prop.oracleFeeRecipient)) + state.mut().oracleFeeRecipient = prop.oracleFeeRecipient; + return true; } - - if (!allTransfersOk) + else if (prop.proposalType == QSBPropUnpause) { - result.reasonCode = QSBReasonTransferFailed; - return result; + state.mut().paused = false; + return true; } - - result.success = true; - return result; + return false; } - inline static ProposeResult tryPropose( - const QPI::QpiContextProcedureCall &qpi, - QPI::ContractState &state, - const Propose_input &input) + inline static sint64 findOracleIndex(const QPI::ContractState &state, const id &account) { - ProposeResult result = { false, QSBReasonNone, 0 }; - - if (qpi.invocationReward() > 0) - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - - sint64 adminIdx = findAdminIndex(state, qpi.invocator()); - if (adminIdx == NULL_INDEX) + for (uint32 i = 0; i < state.get().oracles.capacity(); ++i) { - result.reasonCode = QSBReasonNotAdmin; - return result; + if (state.get().oracles.get(i).active && state.get().oracles.get(i).account == account) + return (sint32)i; } + return NULL_INDEX; + } - if (input.proposalType == 0 || input.proposalType > QSBPropUnpause) + inline static sint64 findPauserIndex(const QPI::ContractState &state, const id &account) + { + for (uint32 i = 0; i < state.get().pausers.capacity(); ++i) { - result.reasonCode = QSBReasonInvalidRole; - return result; + if (state.get().pausers.get(i).active && state.get().pausers.get(i).account == account) + return (sint32)i; } + return NULL_INDEX; + } - if (input.proposalType == QSBPropAddAdmin) - { - if (isZero(input.targetId)) - { - result.reasonCode = QSBReasonInvalidAdmin; - return result; - } - if (findAdminIndex(state, input.targetId) != NULL_INDEX) - { - result.reasonCode = QSBReasonAlreadyAdmin; - return result; - } - if (state.get().adminCount >= QSB_MAX_ADMINS) - { - result.reasonCode = QSBReasonAdminFull; - return result; - } - } - else if (input.proposalType == QSBPropRemoveAdmin) - { - if (isZero(input.targetId)) - { - result.reasonCode = QSBReasonInvalidAdmin; - return result; - } - if (findAdminIndex(state, input.targetId) == NULL_INDEX) - { - result.reasonCode = QSBReasonRoleMissing; - return result; - } - if (state.get().adminCount <= 1) - { - result.reasonCode = QSBReasonWouldLockContract; - return result; - } - if ((state.get().adminCount - 1) < state.get().adminThreshold) - { - result.reasonCode = QSBReasonWouldLockContract; - return result; - } - } - else if (input.proposalType == QSBPropSetAdminThreshold) + // Idempotent insert into ring-buffer filled-order storage. + inline static void markOrderFilled(QPI::ContractState &state, const OrderHash &hash) + { + uint32 i, j; + bool same; + FilledOrderEntry entry; + + for (i = 0; i < state.get().filledOrders.capacity(); ++i) { - if (input.newAdminThreshold == 0 || input.newAdminThreshold > state.get().adminCount) + entry = state.get().filledOrders.get(i); + if (entry.used) { - result.reasonCode = QSBReasonInvalidThreshold; - return result; + same = true; + for (j = 0; j < hash.capacity(); ++j) + { + if (entry.hash.get(j) != hash.get(j)) + { + same = false; + break; + } + } + if (same) + return; } } - else if (input.proposalType == QSBPropAddRole || input.proposalType == QSBPropRemoveRole) + + 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) { - if (isZero(input.targetId)) - { - result.reasonCode = QSBReasonInvalidAdmin; - return result; - } - if (input.role != (uint8)Role::Oracle && input.role != (uint8)Role::Pauser) - { - result.reasonCode = QSBReasonInvalidRole; - return result; - } + // On ring buffer wrap: preserve current buffer as prev, clear current, advance era. + state.mut().filledOrdersPrev = state.get().filledOrders; + setMemory(state.mut().filledOrders, 0); + state.mut().orderEra = state.get().orderEra + 1; } - else if (input.proposalType == QSBPropEditOracleThreshold) + } + + // Checks current and previous era buffers to cover in-flight orders across a ring wrap. + inline static bit isOrderFilled(const QPI::ContractState &state, const OrderHash &hash) + { + uint32 i, j; + bool same; + FilledOrderEntry entry; + + for (i = 0; i < state.get().filledOrders.capacity(); ++i) { - if (input.newOracleThreshold == 0 || input.newOracleThreshold > 100) + entry = state.get().filledOrders.get(i); + if (!entry.used) + continue; + same = true; + for (j = 0; j < hash.capacity(); ++j) { - result.reasonCode = QSBReasonInvalidThreshold; - return result; + if (entry.hash.get(j) != hash.get(j)) + { + same = false; + break; + } } + if (same) + return true; } - else if (input.proposalType == QSBPropEditFeeParameters) + for (i = 0; i < state.get().filledOrdersPrev.capacity(); ++i) { - if (input.bpsFee > QSB_MAX_BPS_FEE || input.protocolFee > QSB_MAX_PROTOCOL_FEE) + entry = state.get().filledOrdersPrev.get(i); + if (!entry.used) + continue; + same = true; + for (j = 0; j < hash.capacity(); ++j) { - result.reasonCode = QSBReasonInvalidFeeParams; - return result; + if (entry.hash.get(j) != hash.get(j)) + { + same = false; + break; + } } + if (same) + return true; } - else if (input.proposalType != QSBPropUnpause) + return false; + } + + inline static sint64 findLockedOrderIndexByNonce(const QPI::ContractState &state, uint32 nonce) + { + for (uint32 i = 0; i < QSB_MAX_LOCKED_ORDERS; ++i) { - result.reasonCode = QSBReasonInvalidProposalType; - return result; + if (state.get().lockedOrders.get(i).active && state.get().lockedOrders.get(i).nonce == nonce) + return (sint32)i; } + return NULL_INDEX; + } - // Enforce per-admin concurrent proposal cap - uint8 adminProposalCount = 0; + // Sweep expired proposals — called from END_EPOCH. + inline static void sweepExpiredProposals( + const QPI::QpiContextProcedureCall &qpi, + QPI::ContractState &state) + { AdminProposal prop; for (uint32 i = 0; i < QSB_MAX_PROPOSALS; ++i) { prop = state.get().proposals.get(i); - if (prop.active && prop.proposer == qpi.invocator()) - ++adminProposalCount; + if (prop.active && qpi.epoch() > prop.createdEpoch + QSB_PROPOSAL_EXPIRY_EPOCHS) + { + prop.active = 0; + state.mut().proposals.set(i, prop); + } } - if (adminProposalCount >= QSB_MAX_PROPOSALS_PER_ADMIN) + } + + // --------------------------------------------------------------------- + // Procedure logic helpers (try*) + // All logic lives here; LOG_INFO stays in the thin procedure shell. + // --------------------------------------------------------------------- + + inline static LockResult tryLock( + const QPI::QpiContextProcedureCall &qpi, + QPI::ContractState &state, + const Lock_input &input) + { + LockResult result = { false, QSBReasonNone, {}, 0 }; + + if (state.get().paused) { - result.reasonCode = QSBReasonTooManyProposals; + if (qpi.invocationReward() > 0) + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + result.reasonCode = QSBReasonPaused; return result; } - uint8 slotIdx = (uint8)QSB_MAX_PROPOSALS; - for (uint32 i = 0; i < QSB_MAX_PROPOSALS; ++i) + if (input.amount == 0 || input.relayerFee >= input.amount) { - if (!state.get().proposals.get(i).active) - { - slotIdx = (uint8)i; - break; - } + if (qpi.invocationReward() > 0) + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + result.reasonCode = QSBReasonInvalidAmount; + return result; } - if (slotIdx >= QSB_MAX_PROPOSALS) + + if (qpi.invocationReward() < (sint64)input.amount) { - result.reasonCode = QSBReasonProposalFull; + if (qpi.invocationReward() > 0) + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + result.reasonCode = QSBReasonInsufficientReward; return result; } - // Build proposal; proposer auto-approves (bit set at their admin index). - setMemory(prop, 0); - prop.proposalType = input.proposalType; - prop.active = 1; - prop.executed = 0; - prop.proposer = qpi.invocator(); - prop.createdEpoch = qpi.epoch(); - prop.approvedMask = (uint8)(1u << (uint8)adminIdx); - prop.approvalCount = 1; - prop.targetId = input.targetId; - prop.role = input.role; - prop.newAdminThreshold = input.newAdminThreshold; - prop.newOracleThreshold = input.newOracleThreshold; - prop.protocolFeeRecipient = input.protocolFeeRecipient; - prop.oracleFeeRecipient = input.oracleFeeRecipient; - prop.bpsFee = input.bpsFee; - prop.protocolFee = input.protocolFee; - state.mut().proposals.set(slotIdx, prop); - result.proposalId = slotIdx; - result.success = true; + // Any excess over `amount` is refunded; exactly `amount` stays locked. + if (qpi.invocationReward() > (sint64)input.amount) + qpi.transfer(qpi.invocator(), qpi.invocationReward() - input.amount); - // Execute immediately when threshold == 1 (single-admin or bootstrap mode). - if (state.get().adminThreshold <= 1) + if (findLockedOrderIndexByNonce(state, input.nonce) != NULL_INDEX) { - bool execOk = executeProposalPayload(state, prop); - prop = state.get().proposals.get(slotIdx); - prop.active = 0; - prop.executed = execOk ? 1 : 0; - state.mut().proposals.set(slotIdx, prop); - if (execOk && - (input.proposalType == QSBPropAddAdmin || - input.proposalType == QSBPropRemoveAdmin || - input.proposalType == QSBPropSetAdminThreshold)) - { - cancelAllPendingProposals(state); - } - result.success = execOk; + qpi.transfer(qpi.invocator(), input.amount); + result.reasonCode = QSBReasonNonceUsed; + return result; } + Order tmpOrder; + tmpOrder.networkIn = 1; + tmpOrder.networkOut = input.networkOut; + setMemory(tmpOrder.tokenIn, 0); + setMemory(tmpOrder.tokenOut, 0); + tmpOrder.fromAddress = qpi.invocator(); + tmpOrder.toAddress = NULL_ID; + tmpOrder.amount = input.amount; + tmpOrder.relayerFee = input.relayerFee; + setMemory(tmpOrder.nonce, 0); + tmpOrder.nonce.set(0, (uint8)(input.nonce & 0xFF)); + tmpOrder.nonce.set(1, (uint8)((input.nonce >> 8) & 0xFF)); + tmpOrder.nonce.set(2, (uint8)((input.nonce >> 16) & 0xFF)); + tmpOrder.nonce.set(3, (uint8)((input.nonce >> 24) & 0xFF)); + tmpOrder.orderEra = state.get().orderEra; + + QSBOrderMessage msgBuffer; + OrderHash tmpIdBytes; + buildOrderMessage(msgBuffer, tmpOrder, tmpIdBytes); + id digest = qpi.K12(msgBuffer); + digestToOrderHash(digest, result.orderHash); + result.orderEra = state.get().orderEra; + + // Persist in ring buffer; oldest slot overwritten when full. + // By the time the ring wraps (1024 orders), off-chain tooling has indexed earlier entries. + LockedOrderEntry entry; + entry.active = true; + entry.sender = qpi.invocator(); + entry.networkOut = input.networkOut; + entry.amount = input.amount; + entry.relayerFee = input.relayerFee; + entry.nonce = input.nonce; + copyMemory(entry.toAddress, input.toAddress); + entry.orderHash = result.orderHash; + entry.lockEpoch = qpi.epoch(); + entry.orderEra = state.get().orderEra; + entry.overrideLockCount = 0; + state.mut().lockedOrders.set(state.get().lastLockedOrdersNextOverwriteIdx, entry); + state.mut().lastLockedOrdersNextOverwriteIdx = + (state.get().lastLockedOrdersNextOverwriteIdx + 1) & (QSB_MAX_LOCKED_ORDERS - 1); + + result.success = true; return result; } - inline static ApproveProposalResult tryApproveProposal( + inline static OverrideLockResult tryOverrideLock( const QPI::QpiContextProcedureCall &qpi, QPI::ContractState &state, - const ApproveProposal_input &input) + const OverrideLock_input &input) { - ApproveProposalResult result = { false, false, QSBReasonNone, 0, NULL_ID, 0 }; + OverrideLockResult result = { false, QSBReasonNone, {}, 0, 0, 0, 0 }; + // Always refund — locking was done in the original lock() call. if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward()); - sint64 adminIdx = findAdminIndex(state, qpi.invocator()); - if (adminIdx == NULL_INDEX) + if (state.get().paused) { - result.reasonCode = QSBReasonNotAdmin; + result.reasonCode = QSBReasonPaused; return result; } - if (input.proposalId >= QSB_MAX_PROPOSALS) + sint64 idx = findLockedOrderIndexByNonce(state, input.nonce); + if (idx == NULL_INDEX) { - result.reasonCode = QSBReasonProposalNotFound; + result.reasonCode = QSBReasonOrderNotFound; return result; } - AdminProposal prop = state.get().proposals.get(input.proposalId); - result.proposalType = prop.proposalType; - result.proposer = prop.proposer; - result.approvalCount = prop.approvalCount; + LockedOrderEntry entry = state.get().lockedOrders.get((uint32)idx); - if (!prop.active) + if (entry.sender != qpi.invocator()) { - result.reasonCode = QSBReasonProposalNotFound; + result.reasonCode = QSBReasonNotSender; return result; } - if (qpi.epoch() > prop.createdEpoch + QSB_PROPOSAL_EXPIRY_EPOCHS) + if (entry.overrideLockCount >= QSB_OVERRIDE_LOCK_MAX_ATTEMPTS) { - prop.active = 0; - state.mut().proposals.set(input.proposalId, prop); - result.reasonCode = QSBReasonProposalExpired; + result.reasonCode = QSBReasonOverrideLimitReached; return result; } - uint8 bitPos = (uint8)adminIdx; - if (bitPos < 8 && (prop.approvedMask & (uint8)(1u << bitPos))) + if (input.relayerFee >= entry.amount) { - result.reasonCode = QSBReasonAlreadyApproved; + result.reasonCode = QSBReasonBadRelayerFee; return result; } - prop.approvedMask |= (uint8)(1u << bitPos); - prop.approvalCount = countBitsUint8(prop.approvedMask); - state.mut().proposals.set(input.proposalId, prop); - result.success = true; - result.approvalCount = prop.approvalCount; + copyMemory(entry.toAddress, input.toAddress); + entry.relayerFee = input.relayerFee; - if (prop.approvalCount >= state.get().adminThreshold) - { - uint8 propType = prop.proposalType; - bool execOk = executeProposalPayload(state, prop); - prop = state.get().proposals.get(input.proposalId); - prop.active = 0; - prop.executed = execOk ? 1 : 0; - state.mut().proposals.set(input.proposalId, prop); - result.executed = true; - if (execOk && - (propType == QSBPropAddAdmin || - propType == QSBPropRemoveAdmin || - propType == QSBPropSetAdminThreshold)) - { - cancelAllPendingProposals(state); - } - } + Order tmpOrder; + tmpOrder.networkIn = 1; + tmpOrder.networkOut = entry.networkOut; + setMemory(tmpOrder.tokenIn, 0); + setMemory(tmpOrder.tokenOut, 0); + tmpOrder.fromAddress = entry.sender; + tmpOrder.toAddress = NULL_ID; + tmpOrder.amount = entry.amount; + tmpOrder.relayerFee = entry.relayerFee; + setMemory(tmpOrder.nonce, 0); + tmpOrder.nonce.set(0, (uint8)(entry.nonce & 0xFF)); + tmpOrder.nonce.set(1, (uint8)((entry.nonce >> 8) & 0xFF)); + tmpOrder.nonce.set(2, (uint8)((entry.nonce >> 16) & 0xFF)); + tmpOrder.nonce.set(3, (uint8)((entry.nonce >> 24) & 0xFF)); + tmpOrder.orderEra = entry.orderEra; // preserve original era + QSBOrderMessage msgBuffer; + OrderHash tmpIdBytes; + buildOrderMessage(msgBuffer, tmpOrder, tmpIdBytes); + id digest = qpi.K12(msgBuffer); + digestToOrderHash(digest, entry.orderHash); + + entry.overrideLockCount++; + state.mut().lockedOrders.set((uint32)idx, entry); + + result.orderHash = entry.orderHash; + result.amount = entry.amount; + result.relayerFee = entry.relayerFee; + result.networkOut = entry.networkOut; + result.orderEra = entry.orderEra; + result.success = true; return result; } - inline static CancelProposalResult tryCancelProposal( + inline static UnlockResult tryUnlock( const QPI::QpiContextProcedureCall &qpi, QPI::ContractState &state, - const CancelProposal_input &input) + const Unlock_input &input) { - CancelProposalResult result = { false, QSBReasonNone, 0, NULL_ID, 0 }; + UnlockResult result = { false, QSBReasonNone, {} }; + + if (state.get().paused) + { + if (qpi.invocationReward() > 0) + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + result.reasonCode = QSBReasonPaused; + return result; + } + // Refund invocation reward — relayer is paid from order.amount, not from reward. if (qpi.invocationReward() > 0) qpi.transfer(qpi.invocator(), qpi.invocationReward()); - if (!isAdmin(state, qpi.invocator())) + if (input.order.amount == 0 || input.order.relayerFee >= input.order.amount) { - result.reasonCode = QSBReasonNotAdmin; + result.reasonCode = QSBReasonInvalidAmount; + return result; + } + + // Defensive balance check — should never fail under normal operation since + // Lock keeps funds inside the contract, but guards against unexpected discrepancies. + Entity entity; + qpi.getEntity(SELF, entity); + uint64 contractBalance = (entity.incomingAmount >= entity.outgoingAmount) + ? entity.incomingAmount - entity.outgoingAmount + : 0; + + if (contractBalance < input.order.amount) + { + result.reasonCode = QSBReasonInsufficientReward; return result; } - if (input.proposalId >= QSB_MAX_PROPOSALS) + // Accept current era or immediately previous era. + // The N-1 grace window covers in-flight orders signed just before a ring-buffer wrap; + // isOrderFilled checks both buffers to prevent replays across the boundary. + if (input.order.orderEra != state.get().orderEra && + !(state.get().orderEra > 0 && input.order.orderEra == state.get().orderEra - 1)) { - result.reasonCode = QSBReasonProposalNotFound; + result.reasonCode = QSBReasonEraMismatch; return result; } - AdminProposal prop = state.get().proposals.get(input.proposalId); - result.proposalType = prop.proposalType; - result.proposer = prop.proposer; - result.approvalCount = prop.approvalCount; + // 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 a balance check. + // This models a fungible lock/mint ↔ burn/unlock bridge where minted tokens + // can be freely transferred and aggregated. - if (!prop.active) + QSBOrderMessage msgBuffer; + OrderHash tmpIdBytes; + buildOrderMessage(msgBuffer, input.order, tmpIdBytes); + id digest = qpi.K12(msgBuffer); + digestToOrderHash(digest, result.orderHash); + + FilledOrderEntry entry; + if (isOrderFilled(state, result.orderHash)) { - result.reasonCode = QSBReasonProposalNotFound; + result.reasonCode = QSBReasonAlreadyFilled; return result; } - if (prop.proposer != qpi.invocator()) + if (state.get().oracleCount == 0 || input.numSignatures == 0) { - result.reasonCode = QSBReasonNotProposer; + result.reasonCode = QSBReasonNoOracles; return result; } - prop.active = 0; - state.mut().proposals.set(input.proposalId, prop); - result.success = true; - return result; - } - - inline static PauseResult tryPause( - const QPI::QpiContextProcedureCall &qpi, - QPI::ContractState &state) - { - PauseResult result = { false, QSBReasonNone }; + // requiredSignatures = ceil(oracleCount * oracleThreshold / 100) + uint128 tmpMul = uint128(state.get().oracleCount) * uint128(state.get().oracleThreshold); + uint128 tmpMul2 = div(tmpMul, uint128(100)); + uint32 requiredSignatures = (uint32)tmpMul2.low; + if (requiredSignatures * 100 < state.get().oracleCount * state.get().oracleThreshold) + ++requiredSignatures; + if (requiredSignatures == 0) + requiredSignatures = 1; - if (qpi.invocationReward() > 0) - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + uint32 validSignatureCount = 0; + uint32 seenCount = 0; + Array seenSigners; - if (!isAdminOrPauser(state, qpi.invocator())) + for (uint32 i = 0; i < input.numSignatures && i < input.signatures.capacity(); ++i) { - result.reasonCode = QSBReasonNotAdminOrPauser; - return result; - } + SignatureData sig = input.signatures.get(i); - state.mut().paused = true; - result.success = true; - return result; - } + if (findOracleIndex(state, sig.signer) == NULL_INDEX) + { + result.reasonCode = QSBReasonInvalidSignature; + return result; + } -public: - // --------------------------------------------------------------------- - // Core user procedures - // --------------------------------------------------------------------- + for (uint32 j = 0; j < seenCount; ++j) + { + if (seenSigners.get(j) == sig.signer) + { + result.reasonCode = QSBReasonDuplicateSigner; + return result; + } + } - struct Lock_locals - { - LockResult result; - QSBLogLockMessage logMsg; - }; + if (!qpi.signatureValidity(sig.signer, digest, sig.signature)) + { + result.reasonCode = QSBReasonInvalidSignature; + return result; + } - PUBLIC_PROCEDURE_WITH_LOCALS(Lock) - { - locals.result = tryLock(qpi, state, input); - output.success = locals.result.success; - output.orderHash = locals.result.orderHash; + if (seenCount < seenSigners.capacity()) + { + seenSigners.set(seenCount, sig.signer); + ++seenCount; + } + ++validSignatureCount; + } - 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; - locals.logMsg.orderHash = locals.result.orderHash; - locals.logMsg.success = locals.result.success ? 1 : 0; - locals.logMsg.reasonCode = locals.result.reasonCode; - locals.logMsg.orderEra = locals.result.orderEra; - locals.logMsg._terminator = 0; - LOG_INFO(locals.logMsg); - } + if (validSignatureCount < requiredSignatures) + { + result.reasonCode = QSBReasonThresholdFailed; + return result; + } - struct OverrideLock_locals - { - OverrideLockResult result; - QSBLogOverrideLockMessage logMsg; - }; + // bpsFeeAmount = netAmount * bpsFee / 10000 + uint64 netAmount = input.order.amount - input.order.relayerFee; + tmpMul = uint128(netAmount) * uint128(state.get().bpsFee); + tmpMul2 = div(tmpMul, uint128(10000)); + uint64 bpsFeeAmount = (uint64)tmpMul2.low; - PUBLIC_PROCEDURE_WITH_LOCALS(OverrideLock) - { - locals.result = tryOverrideLock(qpi, state, input); - output.success = locals.result.success; - output.orderHash = locals.result.orderHash; + // protocolFeeAmount = bpsFeeAmount * protocolFee / 100 + tmpMul = uint128(bpsFeeAmount) * uint128(state.get().protocolFee); + tmpMul2 = div(tmpMul, uint128(100)); + uint64 protocolFeeAmount = (uint64)tmpMul2.low; - locals.logMsg._contractIndex = SELF_INDEX; - locals.logMsg._type = QSBLogOverrideLock; - locals.logMsg.from = qpi.invocator(); - setMemory(locals.logMsg.to, 0); - if (locals.result.success) - copyFromBuffer(locals.logMsg.to, input.toAddress); - locals.logMsg.amount = locals.result.amount; - locals.logMsg.relayerFee = locals.result.relayerFee; - locals.logMsg.networkOut = locals.result.networkOut; - locals.logMsg.nonce = input.nonce; - locals.logMsg.orderHash = locals.result.orderHash; - locals.logMsg.success = locals.result.success ? 1 : 0; - locals.logMsg.reasonCode = locals.result.reasonCode; - locals.logMsg.orderEra = locals.result.orderEra; - locals.logMsg._terminator = 0; - LOG_INFO(locals.logMsg); - } + // oracleFeeAmount = bpsFeeAmount - protocolFeeAmount + uint64 oracleFeeAmount = (bpsFeeAmount >= protocolFeeAmount) ? bpsFeeAmount - protocolFeeAmount : 0; - struct Unlock_locals - { - UnlockResult result; - QSBLogUnlockMessage logMsg; - }; + // recipientAmount = netAmount - bpsFeeAmount + uint64 recipientAmount = (netAmount >= bpsFeeAmount) ? netAmount - bpsFeeAmount : 0; - PUBLIC_PROCEDURE_WITH_LOCALS(Unlock) - { - locals.result = tryUnlock(qpi, state, input); - output.success = locals.result.success; - output.orderHash = locals.result.orderHash; + // Mark filled BEFORE transfers to prevent replay on partial transfer failure. + // The balance check above guarantees the contract has enough funds. + markOrderFilled(state, result.orderHash); - locals.logMsg._contractIndex = SELF_INDEX; - locals.logMsg._type = QSBLogUnlock; - locals.logMsg.orderHash = locals.result.orderHash; - 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.success = locals.result.success ? 1 : 0; - locals.logMsg.reasonCode = locals.result.reasonCode; - locals.logMsg.orderEra = input.order.orderEra; - locals.logMsg._terminator = 0; - LOG_INFO(locals.logMsg); - } + bool allTransfersOk = true; - // View functions - PUBLIC_FUNCTION(GetConfig) - { - output.adminCount = state.get().adminCount; - output.adminThreshold = state.get().adminThreshold; - output.admins = state.get().admins; - 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; - } + // Recipient payout first (most important transfer) + if (recipientAmount > 0 && !isZero(input.order.toAddress)) + { + if (qpi.transfer(input.order.toAddress, (sint64)recipientAmount) < 0) + allTransfersOk = false; + } - PUBLIC_FUNCTION(GetProposal) - { - output.exists = false; - if (input.proposalId < QSB_MAX_PROPOSALS) + if (input.order.relayerFee > 0) { - output.proposal = state.get().proposals.get(input.proposalId); - output.exists = output.proposal.active; + if (qpi.transfer(qpi.invocator(), (sint64)input.order.relayerFee) < 0) + allTransfersOk = false; } - } - struct GetProposals_locals - { - uint32 i; - AdminProposal prop; - }; - PUBLIC_FUNCTION_WITH_LOCALS(GetProposals) - { - output.count = 0; - setMemory(output.proposals, 0); - for (locals.i = 0; locals.i < QSB_MAX_PROPOSALS; ++locals.i) + if (protocolFeeAmount > 0 && !isZero(state.get().protocolFeeRecipient)) { - locals.prop = state.get().proposals.get(locals.i); - if (locals.prop.active) - { - output.proposals.set(output.count, locals.prop); - ++output.count; - } + if (qpi.transfer(state.get().protocolFeeRecipient, (sint64)protocolFeeAmount) < 0) + allTransfersOk = false; } - } - PUBLIC_FUNCTION(IsOracle) - { - output.isOracle = (findOracleIndex(state, input.account) != NULL_INDEX); - } + if (oracleFeeAmount > 0 && !isZero(state.get().oracleFeeRecipient)) + { + if (qpi.transfer(state.get().oracleFeeRecipient, (sint64)oracleFeeAmount) < 0) + allTransfersOk = false; + } + + if (!allTransfersOk) + { + result.reasonCode = QSBReasonTransferFailed; + return result; + } - PUBLIC_FUNCTION(IsPauser) - { - output.isPauser = (findPauserIndex(state, input.account) != NULL_INDEX); + result.success = true; + return result; } - struct GetLockedOrder_locals + inline static ProposeResult tryPropose( + const QPI::QpiContextProcedureCall &qpi, + QPI::ContractState &state, + const Propose_input &input) { - sint64 idx; - }; + ProposeResult result = { false, QSBReasonNone, 0 }; - PUBLIC_FUNCTION_WITH_LOCALS(GetLockedOrder) - { - locals.idx = findLockedOrderIndexByNonce(state, input.nonce); - output.exists = (locals.idx != NULL_INDEX); - if (output.exists) - output.order = state.get().lockedOrders.get((uint32)locals.idx); - } + if (qpi.invocationReward() > 0) + qpi.transfer(qpi.invocator(), qpi.invocationReward()); - PUBLIC_FUNCTION(IsOrderFilled) - { - output.filled = isOrderFilled(state, input.hash); - } + sint64 adminIdx = findAdminIndex(state, qpi.invocator()); + if (adminIdx == NULL_INDEX) + { + result.reasonCode = QSBReasonNotAdmin; + return result; + } - struct ComputeOrderHash_locals - { - id digest; - QSBOrderMessage msgBuffer; - OrderHash tmpIdBytes; - }; + if (input.proposalType == 0 || input.proposalType > QSBPropUnpause) + { + result.reasonCode = QSBReasonInvalidRole; + return result; + } - PUBLIC_FUNCTION_WITH_LOCALS(ComputeOrderHash) - { - buildOrderMessage(locals.msgBuffer, input.order, locals.tmpIdBytes); - locals.digest = qpi.K12(locals.msgBuffer); - output.hash.setMem(locals.digest); - } + if (input.proposalType == QSBPropAddAdmin) + { + if (isZero(input.targetId)) + { + result.reasonCode = QSBReasonInvalidAdmin; + return result; + } + if (findAdminIndex(state, input.targetId) != NULL_INDEX) + { + result.reasonCode = QSBReasonAlreadyAdmin; + return result; + } + if (state.get().adminCount >= QSB_MAX_ADMINS) + { + result.reasonCode = QSBReasonAdminFull; + return result; + } + } + else if (input.proposalType == QSBPropRemoveAdmin) + { + if (isZero(input.targetId)) + { + result.reasonCode = QSBReasonInvalidAdmin; + return result; + } + if (findAdminIndex(state, input.targetId) == NULL_INDEX) + { + result.reasonCode = QSBReasonRoleMissing; + return result; + } + if (state.get().adminCount <= 1) + { + result.reasonCode = QSBReasonWouldLockContract; + return result; + } + if ((state.get().adminCount - 1) < state.get().adminThreshold) + { + result.reasonCode = QSBReasonWouldLockContract; + return result; + } + } + else if (input.proposalType == QSBPropSetAdminThreshold) + { + if (input.newAdminThreshold == 0 || input.newAdminThreshold > state.get().adminCount) + { + result.reasonCode = QSBReasonInvalidThreshold; + return result; + } + } + else if (input.proposalType == QSBPropAddRole || input.proposalType == QSBPropRemoveRole) + { + if (isZero(input.targetId)) + { + result.reasonCode = QSBReasonInvalidAdmin; + return result; + } + if (input.role != (uint8)Role::Oracle && input.role != (uint8)Role::Pauser) + { + result.reasonCode = QSBReasonInvalidRole; + return result; + } + } + else if (input.proposalType == QSBPropEditOracleThreshold) + { + if (input.newOracleThreshold == 0 || input.newOracleThreshold > 100) + { + result.reasonCode = QSBReasonInvalidThreshold; + return result; + } + } + else if (input.proposalType == QSBPropEditFeeParameters) + { + if (input.bpsFee > QSB_MAX_BPS_FEE || input.protocolFee > QSB_MAX_PROTOCOL_FEE) + { + result.reasonCode = QSBReasonInvalidFeeParams; + return result; + } + } + else if (input.proposalType != QSBPropUnpause) + { + result.reasonCode = QSBReasonInvalidProposalType; + return result; + } - struct GetOracles_locals - { - uint32 i; - RoleEntry entry; - }; + // Enforce per-admin concurrent proposal cap + uint8 adminProposalCount = 0; + AdminProposal prop; + for (uint32 i = 0; i < QSB_MAX_PROPOSALS; ++i) + { + prop = state.get().proposals.get(i); + if (prop.active && prop.proposer == qpi.invocator()) + ++adminProposalCount; + } + if (adminProposalCount >= QSB_MAX_PROPOSALS_PER_ADMIN) + { + result.reasonCode = QSBReasonTooManyProposals; + return result; + } - 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) + uint8 slotIdx = (uint8)QSB_MAX_PROPOSALS; + for (uint32 i = 0; i < QSB_MAX_PROPOSALS; ++i) { - locals.entry = state.get().oracles.get(locals.i); - if (locals.entry.active) + if (!state.get().proposals.get(i).active) { - output.accounts.set(output.count, locals.entry.account); - ++output.count; + slotIdx = (uint8)i; + break; } } - } + if (slotIdx >= QSB_MAX_PROPOSALS) + { + result.reasonCode = QSBReasonProposalFull; + return result; + } - struct GetPausers_locals - { - uint32 i; - RoleEntry entry; - }; + // Build proposal; proposer auto-approves (bit set at their admin index). + setMemory(prop, 0); + prop.proposalType = input.proposalType; + prop.active = 1; + prop.executed = 0; + prop.proposer = qpi.invocator(); + prop.createdEpoch = qpi.epoch(); + prop.approvedMask = (uint8)(1u << (uint8)adminIdx); + prop.approvalCount = 1; + prop.targetId = input.targetId; + prop.role = input.role; + prop.newAdminThreshold = input.newAdminThreshold; + prop.newOracleThreshold = input.newOracleThreshold; + prop.protocolFeeRecipient = input.protocolFeeRecipient; + prop.oracleFeeRecipient = input.oracleFeeRecipient; + prop.bpsFee = input.bpsFee; + prop.protocolFee = input.protocolFee; + state.mut().proposals.set(slotIdx, prop); + result.proposalId = slotIdx; + result.success = true; - 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) + // Execute immediately when threshold == 1 (single-admin or bootstrap mode). + if (state.get().adminThreshold <= 1) { - locals.entry = state.get().pausers.get(locals.i); - if (locals.entry.active) + bool execOk = executeProposalPayload(state, prop); + prop = state.get().proposals.get(slotIdx); + prop.active = 0; + prop.executed = execOk ? 1 : 0; + state.mut().proposals.set(slotIdx, prop); + if (execOk && + (input.proposalType == QSBPropAddAdmin || + input.proposalType == QSBPropRemoveAdmin || + input.proposalType == QSBPropSetAdminThreshold)) { - output.accounts.set(output.count, locals.entry.account); - ++output.count; + cancelAllPendingProposals(state); } + result.success = execOk; } + + return result; } - struct GetLockedOrders_locals + inline static ApproveProposalResult tryApproveProposal( + const QPI::QpiContextProcedureCall &qpi, + QPI::ContractState &state, + const ApproveProposal_input &input) { - uint32 i; - uint32 slot; - uint32 totalActive; - uint32 collected; - uint32 effectiveLimit; - LockedOrderEntry entry; - }; + ApproveProposalResult result = { false, false, QSBReasonNone, 0, NULL_ID, 0 }; - 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; - // Iterate most-recent-first: start one slot before the next write position - for (locals.i = 0; locals.i < QSB_MAX_LOCKED_ORDERS; ++locals.i) + if (qpi.invocationReward() > 0) + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + sint64 adminIdx = findAdminIndex(state, qpi.invocator()); + if (adminIdx == NULL_INDEX) + { + result.reasonCode = QSBReasonNotAdmin; + return result; + } + + if (input.proposalId >= QSB_MAX_PROPOSALS) { - locals.slot = (state.get().lastLockedOrdersNextOverwriteIdx + QSB_MAX_LOCKED_ORDERS - 1 - locals.i) & (QSB_MAX_LOCKED_ORDERS - 1); - locals.entry = state.get().lockedOrders.get(locals.slot); - 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; + result.reasonCode = QSBReasonProposalNotFound; + return result; } - output.totalActive = locals.totalActive; - output.returned = locals.collected; - } - struct GetFilledOrders_locals - { - uint32 i; - uint32 slot; - uint32 totalActive; - uint32 collected; - uint32 effectiveLimit; - FilledOrderEntry entry; - }; + AdminProposal prop = state.get().proposals.get(input.proposalId); + result.proposalType = prop.proposalType; + result.proposer = prop.proposer; + result.approvalCount = prop.approvalCount; - 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; - // Iterate most-recent-first: start one slot before the next write position - for (locals.i = 0; locals.i < QSB_MAX_FILLED_ORDERS; ++locals.i) + if (!prop.active) { - locals.slot = (state.get().lastFilledOrdersNextOverwriteIdx + QSB_MAX_FILLED_ORDERS - 1 - locals.i) & (QSB_MAX_FILLED_ORDERS - 1); - locals.entry = state.get().filledOrders.get(locals.slot); - 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; + result.reasonCode = QSBReasonProposalNotFound; + return result; } - output.totalActive = locals.totalActive; - output.returned = locals.collected; - } - // --------------------------------------------------------------------- - // Admin procedures (multisig) - // --------------------------------------------------------------------- + if (qpi.epoch() > prop.createdEpoch + QSB_PROPOSAL_EXPIRY_EPOCHS) + { + prop.active = 0; + state.mut().proposals.set(input.proposalId, prop); + result.reasonCode = QSBReasonProposalExpired; + return result; + } - struct Propose_locals - { - ProposeResult result; - QSBLogProposalMessage logMsg; - }; + uint8 bitPos = (uint8)adminIdx; + if (bitPos < 8 && (prop.approvedMask & (uint8)(1u << bitPos))) + { + result.reasonCode = QSBReasonAlreadyApproved; + return result; + } - PUBLIC_PROCEDURE_WITH_LOCALS(Propose) - { - locals.result = tryPropose(qpi, state, input); - output.success = locals.result.success; - output.reasonCode = locals.result.reasonCode; - output.proposalId = locals.result.proposalId; + prop.approvedMask |= (uint8)(1u << bitPos); + prop.approvalCount = countBitsUint8(prop.approvedMask); + state.mut().proposals.set(input.proposalId, prop); + result.success = true; + result.approvalCount = prop.approvalCount; - locals.logMsg._contractIndex = SELF_INDEX; - locals.logMsg._type = QSBLogProposalCreated; - locals.logMsg.proposalId = output.proposalId; - locals.logMsg.proposalType = input.proposalType; - locals.logMsg.proposer = qpi.invocator(); - locals.logMsg.actor = qpi.invocator(); - locals.logMsg.approvalCount = 1; - locals.logMsg.success = output.success ? 1 : 0; - locals.logMsg.reasonCode = output.reasonCode; - locals.logMsg._terminator = 0; - LOG_INFO(locals.logMsg); - } + if (prop.approvalCount >= state.get().adminThreshold) + { + uint8 propType = prop.proposalType; + bool execOk = executeProposalPayload(state, prop); + prop = state.get().proposals.get(input.proposalId); + prop.active = 0; + prop.executed = execOk ? 1 : 0; + state.mut().proposals.set(input.proposalId, prop); + result.executed = true; + if (execOk && + (propType == QSBPropAddAdmin || + propType == QSBPropRemoveAdmin || + propType == QSBPropSetAdminThreshold)) + { + cancelAllPendingProposals(state); + } + } - struct ApproveProposal_locals - { - ApproveProposalResult result; - QSBLogProposalMessage logMsg; - }; + return result; + } - PUBLIC_PROCEDURE_WITH_LOCALS(ApproveProposal) + inline static CancelProposalResult tryCancelProposal( + const QPI::QpiContextProcedureCall &qpi, + QPI::ContractState &state, + const CancelProposal_input &input) { - locals.result = tryApproveProposal(qpi, state, input); - output.success = locals.result.success; - output.executed = locals.result.executed; - output.reasonCode = locals.result.reasonCode; + CancelProposalResult result = { false, QSBReasonNone, 0, NULL_ID, 0 }; - locals.logMsg._contractIndex = SELF_INDEX; - locals.logMsg._type = locals.result.executed ? QSBLogProposalExecuted : QSBLogProposalApproved; - locals.logMsg.proposalId = input.proposalId; - locals.logMsg.proposalType = locals.result.proposalType; - locals.logMsg.proposer = locals.result.proposer; - locals.logMsg.actor = qpi.invocator(); - locals.logMsg.approvalCount = locals.result.approvalCount; - locals.logMsg.success = output.success ? 1 : 0; - locals.logMsg.reasonCode = output.reasonCode; - locals.logMsg._terminator = 0; - LOG_INFO(locals.logMsg); - } + if (qpi.invocationReward() > 0) + qpi.transfer(qpi.invocator(), qpi.invocationReward()); - struct CancelProposal_locals - { - CancelProposalResult result; - QSBLogProposalMessage logMsg; - }; + if (!isAdmin(state, qpi.invocator())) + { + result.reasonCode = QSBReasonNotAdmin; + return result; + } - PUBLIC_PROCEDURE_WITH_LOCALS(CancelProposal) - { - locals.result = tryCancelProposal(qpi, state, input); - output.success = locals.result.success; - output.reasonCode = locals.result.reasonCode; + if (input.proposalId >= QSB_MAX_PROPOSALS) + { + result.reasonCode = QSBReasonProposalNotFound; + return result; + } - locals.logMsg._contractIndex = SELF_INDEX; - locals.logMsg._type = QSBLogProposalCancelled; - locals.logMsg.proposalId = input.proposalId; - locals.logMsg.proposalType = locals.result.proposalType; - locals.logMsg.proposer = locals.result.proposer; - locals.logMsg.actor = qpi.invocator(); - locals.logMsg.approvalCount = locals.result.approvalCount; - locals.logMsg.success = output.success ? 1 : 0; - locals.logMsg.reasonCode = output.reasonCode; - locals.logMsg._terminator = 0; - LOG_INFO(locals.logMsg); - } + AdminProposal prop = state.get().proposals.get(input.proposalId); + result.proposalType = prop.proposalType; + result.proposer = prop.proposer; + result.approvalCount = prop.approvalCount; - struct Pause_locals - { - PauseResult result; - QSBLogPausedMessage logMsg; - }; + if (!prop.active) + { + result.reasonCode = QSBReasonProposalNotFound; + return result; + } - PUBLIC_PROCEDURE_WITH_LOCALS(Pause) - { - locals.result = tryPause(qpi, state); - output.success = locals.result.success; + if (prop.proposer != qpi.invocator()) + { + result.reasonCode = QSBReasonNotProposer; + return result; + } - locals.logMsg._contractIndex = SELF_INDEX; - locals.logMsg._type = QSBLogPaused; - locals.logMsg.caller = qpi.invocator(); - locals.logMsg.success = output.success ? 1 : 0; - locals.logMsg.reasonCode = locals.result.reasonCode; - locals.logMsg._terminator = 0; - LOG_INFO(locals.logMsg); + prop.active = 0; + state.mut().proposals.set(input.proposalId, prop); + result.success = true; + return result; } - REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + inline static PauseResult tryPause( + const QPI::QpiContextProcedureCall &qpi, + QPI::ContractState &state) { - // 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); - REGISTER_USER_FUNCTION(GetProposal, 11); - REGISTER_USER_FUNCTION(GetProposals, 12); + PauseResult result = { false, QSBReasonNone }; - // User procedures - REGISTER_USER_PROCEDURE(Lock, 1); - REGISTER_USER_PROCEDURE(OverrideLock, 2); - REGISTER_USER_PROCEDURE(Unlock, 3); + if (qpi.invocationReward() > 0) + qpi.transfer(qpi.invocator(), qpi.invocationReward()); - // Emergency pause — single-key, any admin or pauser - REGISTER_USER_PROCEDURE(Pause, 14); + if (!isAdminOrPauser(state, qpi.invocator())) + { + result.reasonCode = QSBReasonNotAdminOrPauser; + return result; + } - // Multisig admin procedures - REGISTER_USER_PROCEDURE(Propose, 20); - REGISTER_USER_PROCEDURE(ApproveProposal, 21); - REGISTER_USER_PROCEDURE(CancelProposal, 22); + state.mut().paused = true; + result.success = true; + return result; } + // --------------------------------------------------------------------- // Epoch processing // --------------------------------------------------------------------- From 771eafa2ca99db217b6d1b055c3f757d986f9eac Mon Sep 17 00:00:00 2001 From: Jean Date: Wed, 10 Jun 2026 11:19:35 +0200 Subject: [PATCH 26/28] refactor(QSB): remove trivial comments throughout contract Removed comments that restate what identifiers/code already express (struct-level labels, numbered I/O lists, REGISTER section labels, function-level descriptions). Kept only non-obvious WHY comments: QPI constraints, ring-buffer semantics, era grace window, refund ordering, fill-before-transfer invariant, auto-approve design note, test-key deployment warnings. Co-Authored-By: Claude Sonnet 4.6 --- src/contracts/QubicSolanaBridge.h | 42 ------------------------------- 1 file changed, 42 deletions(-) diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h index 91f83621..b007233e 100644 --- a/src/contracts/QubicSolanaBridge.h +++ b/src/contracts/QubicSolanaBridge.h @@ -51,13 +51,11 @@ static constexpr uint32 QSBLogProposalApproved = 12; static constexpr uint32 QSBLogProposalExecuted = 13; static constexpr uint32 QSBLogProposalCancelled = 14; -// Multisig admin constants static constexpr uint32 QSB_MAX_ADMINS = 8; // approvedMask is uint8; must stay ≤ 8 static constexpr uint32 QSB_MAX_PROPOSALS = 16; static constexpr uint32 QSB_MAX_PROPOSALS_PER_ADMIN = 3; static constexpr uint32 QSB_PROPOSAL_EXPIRY_EPOCHS = 4; // ~4 weeks -// Proposal types static constexpr uint8 QSBPropAddAdmin = 1; static constexpr uint8 QSBPropRemoveAdmin = 2; static constexpr uint8 QSBPropSetAdminThreshold = 3; @@ -67,7 +65,6 @@ static constexpr uint8 QSBPropEditOracleThreshold = 6; static constexpr uint8 QSBPropEditFeeParameters = 7; static constexpr uint8 QSBPropUnpause = 8; -// Generic reason codes for logging static constexpr uint8 QSBReasonNone = 0; static constexpr uint8 QSBReasonPaused = 1; static constexpr uint8 QSBReasonInvalidAmount = 2; @@ -93,7 +90,6 @@ static constexpr uint8 QSBReasonInvalidAdmin = 21; static constexpr uint8 QSBReasonInvalidRole = 22; static constexpr uint8 QSBReasonOrderNotFound = 23; static constexpr uint8 QSBReasonOverrideLimitReached = 24; -// Multisig admin reason codes static constexpr uint8 QSBReasonProposalNotFound = 25; static constexpr uint8 QSBReasonProposalExpired = 26; static constexpr uint8 QSBReasonAlreadyApproved = 27; @@ -112,7 +108,6 @@ struct QSB2 struct QSB : public ContractBase { public: - // Role identifiers for addRole / removeRole enum class Role : uint8 { Oracle = 1, @@ -137,7 +132,6 @@ struct QSB : public ContractBase uint32 orderEra; }; - // Compact order-hash representation (K12 digest) typedef Array OrderHash; // Signature wrapper compatible with QPI::signatureValidity @@ -147,21 +141,18 @@ struct QSB : public ContractBase 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; @@ -177,7 +168,6 @@ struct QSB : public ContractBase uint8 overrideLockCount; // at +161; 6 bytes padding follow to keep struct at 168 bytes }; - // Logging messages struct QSBLogLockMessage { uint32 _contractIndex; @@ -311,7 +301,6 @@ struct QSB : public ContractBase uint8 approvalCount; // cached popcount of approvedMask uint8 approvedMask; // bit i = admins[i] approved (max 8 admins) - // Payload — fields used depend on proposalType id targetId; // AddAdmin, RemoveAdmin, AddRole/RemoveRole account uint8 role; // AddRole, RemoveRole: (uint8)Role::Oracle or Role::Pauser uint8 newAdminThreshold; // SetAdminThreshold @@ -326,7 +315,6 @@ struct QSB : public ContractBase // User-facing I/O structures // --------------------------------------------------------------------- - // 1) lock() struct Lock_input { uint64 amount; @@ -342,7 +330,6 @@ struct QSB : public ContractBase bit success; }; - // 2) overrideLock() struct OverrideLock_input { Array toAddress; @@ -356,7 +343,6 @@ struct QSB : public ContractBase bit success; }; - // 3) unlock() struct Unlock_input { Order order; @@ -370,7 +356,6 @@ struct QSB : public ContractBase bit success; }; - // 4) transferAdmin() struct TransferAdmin_input { id newAdmin; @@ -381,7 +366,6 @@ struct QSB : public ContractBase bit success; }; - // 5) editOracleThreshold() struct EditOracleThreshold_input { uint8 newThreshold; @@ -393,7 +377,6 @@ struct QSB : public ContractBase bit success; }; - // 6) addRole() struct AddRole_input { id account; @@ -405,7 +388,6 @@ struct QSB : public ContractBase bit success; }; - // 7) removeRole() struct RemoveRole_input { id account; @@ -417,7 +399,6 @@ struct QSB : public ContractBase bit success; }; - // 8) pause() / unpause() struct Pause_input { }; @@ -430,7 +411,6 @@ struct QSB : public ContractBase typedef Pause_input Unpause_input; typedef Pause_output Unpause_output; - // 9) editFeeParameters() struct EditFeeParameters_input { id protocolFeeRecipient; // updated when not zero-id @@ -444,7 +424,6 @@ struct QSB : public ContractBase bit success; }; - // Propose: create a typed admin proposal (proposer auto-approves) struct Propose_input { uint8 proposalType; @@ -569,7 +548,6 @@ struct QSB : public ContractBase bit filled; }; - // ComputeOrderHash: canonical hash for Unlock verification struct ComputeOrderHash_input { Order order; @@ -580,7 +558,6 @@ struct QSB : public ContractBase OrderHash hash; }; - // GetOracles: bulk enumeration of all oracle accounts struct GetOracles_input { }; @@ -591,7 +568,6 @@ struct QSB : public ContractBase Array accounts; }; - // GetPausers: bulk enumeration of all pauser accounts struct GetPausers_input { }; @@ -602,7 +578,6 @@ struct QSB : public ContractBase Array accounts; }; - // GetLockedOrders: paginated enumeration of active locked orders struct GetLockedOrders_input { uint32 offset; // skip this many active entries @@ -616,7 +591,6 @@ struct QSB : public ContractBase Array entries; }; - // GetFilledOrders: paginated enumeration of filled order hashes struct GetFilledOrders_input { uint32 offset; // skip this many filled entries @@ -635,7 +609,6 @@ struct QSB : public ContractBase // --------------------------------------------------------------------- struct StateData { - // Multisig admin (replaces single `id admin`) Array admins; // zero entry = empty slot uint8 adminCount; // number of active admins uint8 adminThreshold; // M in M-of-N (always ≥ 1, always ≤ adminCount) @@ -811,7 +784,6 @@ struct QSB : public ContractBase LOG_INFO(locals.logMsg); } - // View functions PUBLIC_FUNCTION(GetConfig) { output.adminCount = state.get().adminCount; @@ -1122,7 +1094,6 @@ struct QSB : public ContractBase REGISTER_USER_FUNCTIONS_AND_PROCEDURES() { - // View functions REGISTER_USER_FUNCTION(GetConfig, 1); REGISTER_USER_FUNCTION(IsOracle, 2); REGISTER_USER_FUNCTION(IsPauser, 3); @@ -1136,15 +1107,12 @@ struct QSB : public ContractBase REGISTER_USER_FUNCTION(GetProposal, 11); REGISTER_USER_FUNCTION(GetProposals, 12); - // User procedures REGISTER_USER_PROCEDURE(Lock, 1); REGISTER_USER_PROCEDURE(OverrideLock, 2); REGISTER_USER_PROCEDURE(Unlock, 3); - // Emergency pause — single-key, any admin or pauser REGISTER_USER_PROCEDURE(Pause, 14); - // Multisig admin procedures REGISTER_USER_PROCEDURE(Propose, 20); REGISTER_USER_PROCEDURE(ApproveProposal, 21); REGISTER_USER_PROCEDURE(CancelProposal, 22); @@ -1250,7 +1218,6 @@ struct QSB : public ContractBase return false; } - // Cancel all pending proposals — called when the admin set changes to invalidate stale votes. inline static void cancelAllPendingProposals(QPI::ContractState &state) { AdminProposal prop; @@ -1265,7 +1232,6 @@ struct QSB : public ContractBase } } - // Pure state mutation — no qpi access. inline static bool executeProposalPayload(QPI::ContractState &state, const AdminProposal &prop) { uint32 i; @@ -1410,7 +1376,6 @@ struct QSB : public ContractBase return NULL_INDEX; } - // Idempotent insert into ring-buffer filled-order storage. inline static void markOrderFilled(QPI::ContractState &state, const OrderHash &hash) { uint32 i, j; @@ -1506,7 +1471,6 @@ struct QSB : public ContractBase return NULL_INDEX; } - // Sweep expired proposals — called from END_EPOCH. inline static void sweepExpiredProposals( const QPI::QpiContextProcedureCall &qpi, QPI::ContractState &state) @@ -1822,21 +1786,17 @@ struct QSB : public ContractBase return result; } - // bpsFeeAmount = netAmount * bpsFee / 10000 uint64 netAmount = input.order.amount - input.order.relayerFee; tmpMul = uint128(netAmount) * uint128(state.get().bpsFee); tmpMul2 = div(tmpMul, uint128(10000)); uint64 bpsFeeAmount = (uint64)tmpMul2.low; - // protocolFeeAmount = bpsFeeAmount * protocolFee / 100 tmpMul = uint128(bpsFeeAmount) * uint128(state.get().protocolFee); tmpMul2 = div(tmpMul, uint128(100)); uint64 protocolFeeAmount = (uint64)tmpMul2.low; - // oracleFeeAmount = bpsFeeAmount - protocolFeeAmount uint64 oracleFeeAmount = (bpsFeeAmount >= protocolFeeAmount) ? bpsFeeAmount - protocolFeeAmount : 0; - // recipientAmount = netAmount - bpsFeeAmount uint64 recipientAmount = (netAmount >= bpsFeeAmount) ? netAmount - bpsFeeAmount : 0; // Mark filled BEFORE transfers to prevent replay on partial transfer failure. @@ -1845,7 +1805,6 @@ struct QSB : public ContractBase bool allTransfersOk = true; - // Recipient payout first (most important transfer) if (recipientAmount > 0 && !isZero(input.order.toAddress)) { if (qpi.transfer(input.order.toAddress, (sint64)recipientAmount) < 0) @@ -1987,7 +1946,6 @@ struct QSB : public ContractBase return result; } - // Enforce per-admin concurrent proposal cap uint8 adminProposalCount = 0; AdminProposal prop; for (uint32 i = 0; i < QSB_MAX_PROPOSALS; ++i) From f85487df45733d8d3a020a3cb605b3b76931f2cf Mon Sep 17 00:00:00 2001 From: Jean Date: Wed, 10 Jun 2026 14:55:12 +0200 Subject: [PATCH 27/28] revert: restore NUMBER_OF_TRANSACTIONS_PER_TICK to 1024 Previous session incorrectly set the constant to 4096 and introduced a matching gTxRevenuePoints table for 4096 entries. This reverts both files to the correct value (1024, matching main) and removes the dead 4096 table. Co-Authored-By: Claude Sonnet 4.6 --- src/network_messages/common_def.h | 6 +- src/revenue.h | 267 +----------------------------- 2 files changed, 4 insertions(+), 269 deletions(-) diff --git a/src/network_messages/common_def.h b/src/network_messages/common_def.h index afc4aecd..0cfa458b 100644 --- a/src/network_messages/common_def.h +++ b/src/network_messages/common_def.h @@ -1,11 +1,7 @@ #pragma once #define SIGNATURE_SIZE 64 -#ifdef TESTNET -#define NUMBER_OF_TRANSACTIONS_PER_TICK 4096ULL // Must be 2^N -#else -#define NUMBER_OF_TRANSACTIONS_PER_TICK 4096ULL // Must be 2^N -#endif +#define NUMBER_OF_TRANSACTIONS_PER_TICK 1024ULL // Must be 2^N #define MAX_NUMBER_OF_CONTRACTS 1024 // Must be 1024 #define NUMBER_OF_COMPUTORS 676 #define QUORUM (NUMBER_OF_COMPUTORS * 2 / 3 + 1) diff --git a/src/revenue.h b/src/revenue.h index 5f5ab3e6..eb1fcb6b 100644 --- a/src/revenue.h +++ b/src/revenue.h @@ -23,269 +23,9 @@ static constexpr unsigned long long gVoteScoreScalingThreshold = (1ULL << 10); static unsigned long long gCustomMiningScoreFactor[NUMBER_OF_COMPUTORS]; static constexpr unsigned long long gCustomMiningScoreScalingThreshold = (1ULL << 10); -// gTxRevenuePoints is calculated from 4096 * ln(tx + 1) -// When NUMBER_OF_TRANSACTIONS_PER_TICK is changed, this table needs to be regenerated -// Make sure it is STRICTLY MONOTONIC integer -static constexpr unsigned short gTxRevenuePoints[1 + 4096] = { - 0, 2839, 4500, 5678, 6592, 7339, 7970, 8517, 9000, 9431, 9822, 10178, 10506, 10810, 11092, 11357, - 11605, 11839, 12060, 12271, 12470, 12661, 12843, 13017, 13185, 13345, 13500, 13649, 13792, 13931, 14066, 14196, - 14322, 14444, 14563, 14678, 14790, 14900, 15006, 15110, 15211, 15309, 15406, 15500, 15592, 15682, 15770, 15856, - 15941, 16024, 16105, 16184, 16262, 16339, 16414, 16488, 16560, 16632, 16702, 16770, 16838, 16905, 16970, 17035, - 17098, 17161, 17222, 17283, 17343, 17402, 17460, 17517, 17574, 17629, 17684, 17739, 17792, 17845, 17897, 17949, - 18000, 18050, 18100, 18149, 18197, 18245, 18292, 18339, 18385, 18431, 18476, 18521, 18566, 18609, 18653, 18696, - 18738, 18780, 18822, 18863, 18904, 18944, 18984, 19023, 19063, 19101, 19140, 19178, 19216, 19253, 19290, 19327, - 19363, 19399, 19435, 19471, 19506, 19541, 19575, 19610, 19644, 19677, 19711, 19744, 19777, 19809, 19842, 19874, - 19906, 19937, 19969, 20000, 20031, 20062, 20092, 20122, 20152, 20182, 20212, 20241, 20270, 20299, 20328, 20356, - 20385, 20413, 20441, 20469, 20496, 20524, 20551, 20578, 20605, 20631, 20658, 20684, 20710, 20736, 20762, 20788, - 20813, 20839, 20864, 20889, 20914, 20939, 20963, 20988, 21012, 21036, 21060, 21084, 21108, 21131, 21155, 21178, - 21202, 21225, 21248, 21270, 21293, 21316, 21338, 21360, 21383, 21405, 21427, 21448, 21470, 21492, 21513, 21535, - 21556, 21577, 21598, 21619, 21640, 21661, 21681, 21702, 21722, 21743, 21763, 21783, 21803, 21823, 21843, 21863, - 21882, 21902, 21921, 21941, 21960, 21979, 21998, 22017, 22036, 22055, 22074, 22092, 22111, 22129, 22148, 22166, - 22184, 22203, 22221, 22239, 22257, 22274, 22292, 22310, 22327, 22345, 22362, 22380, 22397, 22414, 22432, 22449, - 22466, 22483, 22500, 22516, 22533, 22550, 22566, 22583, 22599, 22616, 22632, 22649, 22665, 22681, 22697, 22713, - 22729, 22745, 22761, 22777, 22792, 22808, 22824, 22839, 22855, 22870, 22885, 22901, 22916, 22931, 22946, 22961, - 22976, 22991, 23006, 23021, 23036, 23051, 23065, 23080, 23095, 23109, 23124, 23138, 23153, 23167, 23181, 23195, - 23210, 23224, 23238, 23252, 23266, 23280, 23294, 23308, 23322, 23335, 23349, 23363, 23376, 23390, 23403, 23417, - 23430, 23444, 23457, 23470, 23484, 23497, 23510, 23523, 23536, 23550, 23563, 23576, 23588, 23601, 23614, 23627, - 23640, 23653, 23665, 23678, 23691, 23703, 23716, 23728, 23741, 23753, 23765, 23778, 23790, 23802, 23815, 23827, - 23839, 23851, 23863, 23875, 23887, 23899, 23911, 23923, 23935, 23947, 23959, 23971, 23982, 23994, 24006, 24017, - 24029, 24041, 24052, 24064, 24075, 24087, 24098, 24109, 24121, 24132, 24143, 24155, 24166, 24177, 24188, 24200, - 24211, 24222, 24233, 24244, 24255, 24266, 24277, 24288, 24298, 24309, 24320, 24331, 24342, 24352, 24363, 24374, - 24384, 24395, 24406, 24416, 24427, 24437, 24448, 24458, 24469, 24479, 24490, 24500, 24510, 24521, 24531, 24541, - 24551, 24561, 24572, 24582, 24592, 24602, 24612, 24622, 24632, 24642, 24652, 24662, 24672, 24682, 24692, 24702, - 24712, 24721, 24731, 24741, 24751, 24760, 24770, 24780, 24789, 24799, 24809, 24818, 24828, 24837, 24847, 24856, - 24866, 24875, 24885, 24894, 24903, 24913, 24922, 24931, 24941, 24950, 24959, 24968, 24978, 24987, 24996, 25005, - 25014, 25023, 25033, 25042, 25051, 25060, 25069, 25078, 25087, 25096, 25105, 25114, 25122, 25131, 25140, 25149, - 25158, 25167, 25175, 25184, 25193, 25202, 25210, 25219, 25228, 25236, 25245, 25254, 25262, 25271, 25279, 25288, - 25296, 25305, 25313, 25322, 25330, 25339, 25347, 25356, 25364, 25372, 25381, 25389, 25397, 25406, 25414, 25422, - 25430, 25439, 25447, 25455, 25463, 25471, 25480, 25488, 25496, 25504, 25512, 25520, 25528, 25536, 25544, 25552, - 25560, 25568, 25576, 25584, 25592, 25600, 25608, 25616, 25624, 25631, 25639, 25647, 25655, 25663, 25670, 25678, - 25686, 25694, 25701, 25709, 25717, 25725, 25732, 25740, 25747, 25755, 25763, 25770, 25778, 25785, 25793, 25800, - 25808, 25816, 25823, 25831, 25838, 25845, 25853, 25860, 25868, 25875, 25882, 25890, 25897, 25905, 25912, 25919, - 25927, 25934, 25941, 25948, 25956, 25963, 25970, 25977, 25985, 25992, 25999, 26006, 26013, 26020, 26027, 26035, - 26042, 26049, 26056, 26063, 26070, 26077, 26084, 26091, 26098, 26105, 26112, 26119, 26126, 26133, 26140, 26147, - 26154, 26161, 26168, 26174, 26181, 26188, 26195, 26202, 26209, 26215, 26222, 26229, 26236, 26243, 26249, 26256, - 26263, 26270, 26276, 26283, 26290, 26296, 26303, 26310, 26316, 26323, 26330, 26336, 26343, 26349, 26356, 26362, - 26369, 26376, 26382, 26389, 26395, 26402, 26408, 26415, 26421, 26428, 26434, 26440, 26447, 26453, 26460, 26466, - 26473, 26479, 26485, 26492, 26498, 26504, 26511, 26517, 26523, 26530, 26536, 26542, 26549, 26555, 26561, 26567, - 26574, 26580, 26586, 26592, 26598, 26605, 26611, 26617, 26623, 26629, 26635, 26642, 26648, 26654, 26660, 26666, - 26672, 26678, 26684, 26690, 26696, 26702, 26708, 26714, 26721, 26727, 26733, 26739, 26744, 26750, 26756, 26762, - 26768, 26774, 26780, 26786, 26792, 26798, 26804, 26810, 26816, 26822, 26827, 26833, 26839, 26845, 26851, 26857, - 26862, 26868, 26874, 26880, 26886, 26891, 26897, 26903, 26909, 26914, 26920, 26926, 26932, 26937, 26943, 26949, - 26954, 26960, 26966, 26971, 26977, 26983, 26988, 26994, 26999, 27005, 27011, 27016, 27022, 27027, 27033, 27039, - 27044, 27050, 27055, 27061, 27066, 27072, 27077, 27083, 27088, 27094, 27099, 27105, 27110, 27116, 27121, 27127, - 27132, 27138, 27143, 27148, 27154, 27159, 27165, 27170, 27175, 27181, 27186, 27192, 27197, 27202, 27208, 27213, - 27218, 27224, 27229, 27234, 27240, 27245, 27250, 27255, 27261, 27266, 27271, 27276, 27282, 27287, 27292, 27297, - 27303, 27308, 27313, 27318, 27323, 27329, 27334, 27339, 27344, 27349, 27354, 27360, 27365, 27370, 27375, 27380, - 27385, 27390, 27396, 27401, 27406, 27411, 27416, 27421, 27426, 27431, 27436, 27441, 27446, 27451, 27456, 27461, - 27466, 27471, 27476, 27481, 27486, 27491, 27496, 27501, 27506, 27511, 27516, 27521, 27526, 27531, 27536, 27541, - 27546, 27551, 27556, 27560, 27565, 27570, 27575, 27580, 27585, 27590, 27595, 27599, 27604, 27609, 27614, 27619, - 27624, 27628, 27633, 27638, 27643, 27648, 27653, 27657, 27662, 27667, 27672, 27676, 27681, 27686, 27691, 27695, - 27700, 27705, 27710, 27714, 27719, 27724, 27728, 27733, 27738, 27743, 27747, 27752, 27757, 27761, 27766, 27771, - 27775, 27780, 27785, 27789, 27794, 27798, 27803, 27808, 27812, 27817, 27821, 27826, 27831, 27835, 27840, 27844, - 27849, 27853, 27858, 27863, 27867, 27872, 27876, 27881, 27885, 27890, 27894, 27899, 27903, 27908, 27912, 27917, - 27921, 27926, 27930, 27935, 27939, 27944, 27948, 27953, 27957, 27962, 27966, 27970, 27975, 27979, 27984, 27988, - 27993, 27997, 28001, 28006, 28010, 28014, 28019, 28023, 28028, 28032, 28036, 28041, 28045, 28049, 28054, 28058, - 28062, 28067, 28071, 28075, 28080, 28084, 28088, 28093, 28097, 28101, 28106, 28110, 28114, 28118, 28123, 28127, - 28131, 28135, 28140, 28144, 28148, 28152, 28157, 28161, 28165, 28169, 28174, 28178, 28182, 28186, 28190, 28195, - 28199, 28203, 28207, 28211, 28216, 28220, 28224, 28228, 28232, 28236, 28241, 28245, 28249, 28253, 28257, 28261, - 28265, 28270, 28274, 28278, 28282, 28286, 28290, 28294, 28298, 28302, 28306, 28311, 28315, 28319, 28323, 28327, - 28331, 28335, 28339, 28343, 28347, 28351, 28355, 28359, 28363, 28367, 28371, 28375, 28379, 28383, 28387, 28391, - 28395, 28399, 28403, 28407, 28411, 28415, 28419, 28423, 28427, 28431, 28435, 28439, 28443, 28447, 28451, 28455, - 28459, 28463, 28467, 28471, 28474, 28478, 28482, 28486, 28490, 28494, 28498, 28502, 28506, 28510, 28513, 28517, - 28521, 28525, 28529, 28533, 28537, 28541, 28544, 28548, 28552, 28556, 28560, 28564, 28567, 28571, 28575, 28579, - 28583, 28587, 28590, 28594, 28598, 28602, 28606, 28609, 28613, 28617, 28621, 28625, 28628, 28632, 28636, 28640, - 28643, 28647, 28651, 28655, 28658, 28662, 28666, 28670, 28673, 28677, 28681, 28685, 28688, 28692, 28696, 28699, - 28703, 28707, 28711, 28714, 28718, 28722, 28725, 28729, 28733, 28736, 28740, 28744, 28747, 28751, 28755, 28758, - 28762, 28766, 28769, 28773, 28777, 28780, 28784, 28788, 28791, 28795, 28798, 28802, 28806, 28809, 28813, 28816, - 28820, 28824, 28827, 28831, 28834, 28838, 28842, 28845, 28849, 28852, 28856, 28860, 28863, 28867, 28870, 28874, - 28877, 28881, 28884, 28888, 28891, 28895, 28899, 28902, 28906, 28909, 28913, 28916, 28920, 28923, 28927, 28930, - 28934, 28937, 28941, 28944, 28948, 28951, 28955, 28958, 28962, 28965, 28969, 28972, 28976, 28979, 28983, 28986, - 28989, 28993, 28996, 29000, 29003, 29007, 29010, 29014, 29017, 29020, 29024, 29027, 29031, 29034, 29038, 29041, - 29044, 29048, 29051, 29055, 29058, 29061, 29065, 29068, 29072, 29075, 29078, 29082, 29085, 29088, 29092, 29095, - 29099, 29102, 29105, 29109, 29112, 29115, 29119, 29122, 29125, 29129, 29132, 29135, 29139, 29142, 29145, 29149, - 29152, 29155, 29159, 29162, 29165, 29169, 29172, 29175, 29179, 29182, 29185, 29188, 29192, 29195, 29198, 29202, - 29205, 29208, 29211, 29215, 29218, 29221, 29225, 29228, 29231, 29234, 29238, 29241, 29244, 29247, 29251, 29254, - 29257, 29260, 29263, 29267, 29270, 29273, 29276, 29280, 29283, 29286, 29289, 29292, 29296, 29299, 29302, 29305, - 29309, 29312, 29315, 29318, 29321, 29324, 29328, 29331, 29334, 29337, 29340, 29344, 29347, 29350, 29353, 29356, - 29359, 29363, 29366, 29369, 29372, 29375, 29378, 29381, 29385, 29388, 29391, 29394, 29397, 29400, 29403, 29406, - 29410, 29413, 29416, 29419, 29422, 29425, 29428, 29431, 29434, 29438, 29441, 29444, 29447, 29450, 29453, 29456, - 29459, 29462, 29465, 29468, 29471, 29475, 29478, 29481, 29484, 29487, 29490, 29493, 29496, 29499, 29502, 29505, - 29508, 29511, 29514, 29517, 29520, 29523, 29526, 29529, 29532, 29536, 29539, 29542, 29545, 29548, 29551, 29554, - 29557, 29560, 29563, 29566, 29569, 29572, 29575, 29578, 29581, 29584, 29587, 29590, 29593, 29596, 29599, 29602, - 29605, 29607, 29610, 29613, 29616, 29619, 29622, 29625, 29628, 29631, 29634, 29637, 29640, 29643, 29646, 29649, - 29652, 29655, 29658, 29661, 29664, 29667, 29669, 29672, 29675, 29678, 29681, 29684, 29687, 29690, 29693, 29696, - 29699, 29702, 29704, 29707, 29710, 29713, 29716, 29719, 29722, 29725, 29728, 29730, 29733, 29736, 29739, 29742, - 29745, 29748, 29751, 29753, 29756, 29759, 29762, 29765, 29768, 29771, 29773, 29776, 29779, 29782, 29785, 29788, - 29791, 29793, 29796, 29799, 29802, 29805, 29808, 29810, 29813, 29816, 29819, 29822, 29825, 29827, 29830, 29833, - 29836, 29839, 29841, 29844, 29847, 29850, 29853, 29855, 29858, 29861, 29864, 29867, 29869, 29872, 29875, 29878, - 29881, 29883, 29886, 29889, 29892, 29894, 29897, 29900, 29903, 29906, 29908, 29911, 29914, 29917, 29919, 29922, - 29925, 29928, 29930, 29933, 29936, 29939, 29941, 29944, 29947, 29949, 29952, 29955, 29958, 29960, 29963, 29966, - 29969, 29971, 29974, 29977, 29979, 29982, 29985, 29988, 29990, 29993, 29996, 29998, 30001, 30004, 30007, 30009, - 30012, 30015, 30017, 30020, 30023, 30025, 30028, 30031, 30033, 30036, 30039, 30041, 30044, 30047, 30049, 30052, - 30055, 30057, 30060, 30063, 30065, 30068, 30071, 30073, 30076, 30079, 30081, 30084, 30087, 30089, 30092, 30095, - 30097, 30100, 30102, 30105, 30108, 30110, 30113, 30116, 30118, 30121, 30123, 30126, 30129, 30131, 30134, 30137, - 30139, 30142, 30144, 30147, 30150, 30152, 30155, 30157, 30160, 30163, 30165, 30168, 30170, 30173, 30176, 30178, - 30181, 30183, 30186, 30188, 30191, 30194, 30196, 30199, 30201, 30204, 30206, 30209, 30212, 30214, 30217, 30219, - 30222, 30224, 30227, 30230, 30232, 30235, 30237, 30240, 30242, 30245, 30247, 30250, 30252, 30255, 30258, 30260, - 30263, 30265, 30268, 30270, 30273, 30275, 30278, 30280, 30283, 30285, 30288, 30290, 30293, 30295, 30298, 30300, - 30303, 30305, 30308, 30310, 30313, 30315, 30318, 30320, 30323, 30325, 30328, 30330, 30333, 30335, 30338, 30340, - 30343, 30345, 30348, 30350, 30353, 30355, 30358, 30360, 30363, 30365, 30368, 30370, 30373, 30375, 30377, 30380, - 30382, 30385, 30387, 30390, 30392, 30395, 30397, 30400, 30402, 30404, 30407, 30409, 30412, 30414, 30417, 30419, - 30422, 30424, 30426, 30429, 30431, 30434, 30436, 30439, 30441, 30443, 30446, 30448, 30451, 30453, 30456, 30458, - 30460, 30463, 30465, 30468, 30470, 30472, 30475, 30477, 30480, 30482, 30484, 30487, 30489, 30492, 30494, 30496, - 30499, 30501, 30504, 30506, 30508, 30511, 30513, 30516, 30518, 30520, 30523, 30525, 30527, 30530, 30532, 30535, - 30537, 30539, 30542, 30544, 30546, 30549, 30551, 30553, 30556, 30558, 30561, 30563, 30565, 30568, 30570, 30572, - 30575, 30577, 30579, 30582, 30584, 30586, 30589, 30591, 30593, 30596, 30598, 30600, 30603, 30605, 30607, 30610, - 30612, 30614, 30617, 30619, 30621, 30624, 30626, 30628, 30631, 30633, 30635, 30638, 30640, 30642, 30644, 30647, - 30649, 30651, 30654, 30656, 30658, 30661, 30663, 30665, 30667, 30670, 30672, 30674, 30677, 30679, 30681, 30683, - 30686, 30688, 30690, 30693, 30695, 30697, 30699, 30702, 30704, 30706, 30709, 30711, 30713, 30715, 30718, 30720, - 30722, 30724, 30727, 30729, 30731, 30733, 30736, 30738, 30740, 30742, 30745, 30747, 30749, 30751, 30754, 30756, - 30758, 30760, 30763, 30765, 30767, 30769, 30772, 30774, 30776, 30778, 30781, 30783, 30785, 30787, 30790, 30792, - 30794, 30796, 30798, 30801, 30803, 30805, 30807, 30810, 30812, 30814, 30816, 30818, 30821, 30823, 30825, 30827, - 30829, 30832, 30834, 30836, 30838, 30840, 30843, 30845, 30847, 30849, 30851, 30854, 30856, 30858, 30860, 30862, - 30865, 30867, 30869, 30871, 30873, 30875, 30878, 30880, 30882, 30884, 30886, 30889, 30891, 30893, 30895, 30897, - 30899, 30902, 30904, 30906, 30908, 30910, 30912, 30915, 30917, 30919, 30921, 30923, 30925, 30928, 30930, 30932, - 30934, 30936, 30938, 30940, 30943, 30945, 30947, 30949, 30951, 30953, 30955, 30958, 30960, 30962, 30964, 30966, - 30968, 30970, 30972, 30975, 30977, 30979, 30981, 30983, 30985, 30987, 30989, 30992, 30994, 30996, 30998, 31000, - 31002, 31004, 31006, 31009, 31011, 31013, 31015, 31017, 31019, 31021, 31023, 31025, 31027, 31030, 31032, 31034, - 31036, 31038, 31040, 31042, 31044, 31046, 31048, 31051, 31053, 31055, 31057, 31059, 31061, 31063, 31065, 31067, - 31069, 31071, 31073, 31076, 31078, 31080, 31082, 31084, 31086, 31088, 31090, 31092, 31094, 31096, 31098, 31100, - 31102, 31105, 31107, 31109, 31111, 31113, 31115, 31117, 31119, 31121, 31123, 31125, 31127, 31129, 31131, 31133, - 31135, 31137, 31139, 31141, 31144, 31146, 31148, 31150, 31152, 31154, 31156, 31158, 31160, 31162, 31164, 31166, - 31168, 31170, 31172, 31174, 31176, 31178, 31180, 31182, 31184, 31186, 31188, 31190, 31192, 31194, 31196, 31198, - 31200, 31202, 31204, 31206, 31208, 31210, 31212, 31214, 31216, 31218, 31220, 31222, 31224, 31226, 31228, 31230, - 31232, 31234, 31236, 31238, 31240, 31242, 31244, 31246, 31248, 31250, 31252, 31254, 31256, 31258, 31260, 31262, - 31264, 31266, 31268, 31270, 31272, 31274, 31276, 31278, 31280, 31282, 31284, 31286, 31288, 31290, 31292, 31294, - 31296, 31298, 31300, 31302, 31304, 31306, 31308, 31310, 31312, 31314, 31316, 31318, 31319, 31321, 31323, 31325, - 31327, 31329, 31331, 31333, 31335, 31337, 31339, 31341, 31343, 31345, 31347, 31349, 31351, 31353, 31355, 31356, - 31358, 31360, 31362, 31364, 31366, 31368, 31370, 31372, 31374, 31376, 31378, 31380, 31382, 31384, 31385, 31387, - 31389, 31391, 31393, 31395, 31397, 31399, 31401, 31403, 31405, 31407, 31409, 31410, 31412, 31414, 31416, 31418, - 31420, 31422, 31424, 31426, 31428, 31430, 31431, 31433, 31435, 31437, 31439, 31441, 31443, 31445, 31447, 31449, - 31450, 31452, 31454, 31456, 31458, 31460, 31462, 31464, 31466, 31467, 31469, 31471, 31473, 31475, 31477, 31479, - 31481, 31483, 31484, 31486, 31488, 31490, 31492, 31494, 31496, 31498, 31499, 31501, 31503, 31505, 31507, 31509, - 31511, 31513, 31514, 31516, 31518, 31520, 31522, 31524, 31526, 31527, 31529, 31531, 31533, 31535, 31537, 31539, - 31540, 31542, 31544, 31546, 31548, 31550, 31552, 31553, 31555, 31557, 31559, 31561, 31563, 31564, 31566, 31568, - 31570, 31572, 31574, 31575, 31577, 31579, 31581, 31583, 31585, 31587, 31588, 31590, 31592, 31594, 31596, 31597, - 31599, 31601, 31603, 31605, 31607, 31608, 31610, 31612, 31614, 31616, 31618, 31619, 31621, 31623, 31625, 31627, - 31628, 31630, 31632, 31634, 31636, 31638, 31639, 31641, 31643, 31645, 31647, 31648, 31650, 31652, 31654, 31656, - 31657, 31659, 31661, 31663, 31665, 31666, 31668, 31670, 31672, 31674, 31675, 31677, 31679, 31681, 31683, 31684, - 31686, 31688, 31690, 31691, 31693, 31695, 31697, 31699, 31700, 31702, 31704, 31706, 31708, 31709, 31711, 31713, - 31715, 31716, 31718, 31720, 31722, 31724, 31725, 31727, 31729, 31731, 31732, 31734, 31736, 31738, 31739, 31741, - 31743, 31745, 31747, 31748, 31750, 31752, 31754, 31755, 31757, 31759, 31761, 31762, 31764, 31766, 31768, 31769, - 31771, 31773, 31775, 31776, 31778, 31780, 31782, 31783, 31785, 31787, 31789, 31790, 31792, 31794, 31796, 31797, - 31799, 31801, 31803, 31804, 31806, 31808, 31810, 31811, 31813, 31815, 31816, 31818, 31820, 31822, 31823, 31825, - 31827, 31829, 31830, 31832, 31834, 31835, 31837, 31839, 31841, 31842, 31844, 31846, 31848, 31849, 31851, 31853, - 31854, 31856, 31858, 31860, 31861, 31863, 31865, 31866, 31868, 31870, 31872, 31873, 31875, 31877, 31878, 31880, - 31882, 31883, 31885, 31887, 31889, 31890, 31892, 31894, 31895, 31897, 31899, 31901, 31902, 31904, 31906, 31907, - 31909, 31911, 31912, 31914, 31916, 31917, 31919, 31921, 31923, 31924, 31926, 31928, 31929, 31931, 31933, 31934, - 31936, 31938, 31939, 31941, 31943, 31944, 31946, 31948, 31949, 31951, 31953, 31954, 31956, 31958, 31960, 31961, - 31963, 31965, 31966, 31968, 31970, 31971, 31973, 31975, 31976, 31978, 31980, 31981, 31983, 31985, 31986, 31988, - 31990, 31991, 31993, 31995, 31996, 31998, 32000, 32001, 32003, 32004, 32006, 32008, 32009, 32011, 32013, 32014, - 32016, 32018, 32019, 32021, 32023, 32024, 32026, 32028, 32029, 32031, 32033, 32034, 32036, 32037, 32039, 32041, - 32042, 32044, 32046, 32047, 32049, 32051, 32052, 32054, 32055, 32057, 32059, 32060, 32062, 32064, 32065, 32067, - 32069, 32070, 32072, 32073, 32075, 32077, 32078, 32080, 32082, 32083, 32085, 32086, 32088, 32090, 32091, 32093, - 32095, 32096, 32098, 32099, 32101, 32103, 32104, 32106, 32107, 32109, 32111, 32112, 32114, 32116, 32117, 32119, - 32120, 32122, 32124, 32125, 32127, 32128, 32130, 32132, 32133, 32135, 32136, 32138, 32140, 32141, 32143, 32144, - 32146, 32148, 32149, 32151, 32152, 32154, 32156, 32157, 32159, 32160, 32162, 32164, 32165, 32167, 32168, 32170, - 32172, 32173, 32175, 32176, 32178, 32179, 32181, 32183, 32184, 32186, 32187, 32189, 32191, 32192, 32194, 32195, - 32197, 32198, 32200, 32202, 32203, 32205, 32206, 32208, 32210, 32211, 32213, 32214, 32216, 32217, 32219, 32221, - 32222, 32224, 32225, 32227, 32228, 32230, 32232, 32233, 32235, 32236, 32238, 32239, 32241, 32242, 32244, 32246, - 32247, 32249, 32250, 32252, 32253, 32255, 32256, 32258, 32260, 32261, 32263, 32264, 32266, 32267, 32269, 32270, - 32272, 32274, 32275, 32277, 32278, 32280, 32281, 32283, 32284, 32286, 32288, 32289, 32291, 32292, 32294, 32295, - 32297, 32298, 32300, 32301, 32303, 32304, 32306, 32308, 32309, 32311, 32312, 32314, 32315, 32317, 32318, 32320, - 32321, 32323, 32324, 32326, 32327, 32329, 32331, 32332, 32334, 32335, 32337, 32338, 32340, 32341, 32343, 32344, - 32346, 32347, 32349, 32350, 32352, 32353, 32355, 32356, 32358, 32359, 32361, 32363, 32364, 32366, 32367, 32369, - 32370, 32372, 32373, 32375, 32376, 32378, 32379, 32381, 32382, 32384, 32385, 32387, 32388, 32390, 32391, 32393, - 32394, 32396, 32397, 32399, 32400, 32402, 32403, 32405, 32406, 32408, 32409, 32411, 32412, 32414, 32415, 32417, - 32418, 32420, 32421, 32423, 32424, 32426, 32427, 32429, 32430, 32432, 32433, 32435, 32436, 32438, 32439, 32441, - 32442, 32444, 32445, 32447, 32448, 32450, 32451, 32453, 32454, 32456, 32457, 32458, 32460, 32461, 32463, 32464, - 32466, 32467, 32469, 32470, 32472, 32473, 32475, 32476, 32478, 32479, 32481, 32482, 32484, 32485, 32487, 32488, - 32489, 32491, 32492, 32494, 32495, 32497, 32498, 32500, 32501, 32503, 32504, 32506, 32507, 32509, 32510, 32511, - 32513, 32514, 32516, 32517, 32519, 32520, 32522, 32523, 32525, 32526, 32528, 32529, 32530, 32532, 32533, 32535, - 32536, 32538, 32539, 32541, 32542, 32544, 32545, 32546, 32548, 32549, 32551, 32552, 32554, 32555, 32557, 32558, - 32559, 32561, 32562, 32564, 32565, 32567, 32568, 32570, 32571, 32572, 32574, 32575, 32577, 32578, 32580, 32581, - 32583, 32584, 32585, 32587, 32588, 32590, 32591, 32593, 32594, 32595, 32597, 32598, 32600, 32601, 32603, 32604, - 32605, 32607, 32608, 32610, 32611, 32613, 32614, 32615, 32617, 32618, 32620, 32621, 32623, 32624, 32625, 32627, - 32628, 32630, 32631, 32633, 32634, 32635, 32637, 32638, 32640, 32641, 32642, 32644, 32645, 32647, 32648, 32650, - 32651, 32652, 32654, 32655, 32657, 32658, 32659, 32661, 32662, 32664, 32665, 32667, 32668, 32669, 32671, 32672, - 32674, 32675, 32676, 32678, 32679, 32681, 32682, 32683, 32685, 32686, 32688, 32689, 32690, 32692, 32693, 32695, - 32696, 32697, 32699, 32700, 32702, 32703, 32704, 32706, 32707, 32709, 32710, 32711, 32713, 32714, 32716, 32717, - 32718, 32720, 32721, 32722, 32724, 32725, 32727, 32728, 32729, 32731, 32732, 32734, 32735, 32736, 32738, 32739, - 32740, 32742, 32743, 32745, 32746, 32747, 32749, 32750, 32752, 32753, 32754, 32756, 32757, 32758, 32760, 32761, - 32763, 32764, 32765, 32767, 32768, 32769, 32771, 32772, 32774, 32775, 32776, 32778, 32779, 32780, 32782, 32783, - 32785, 32786, 32787, 32789, 32790, 32791, 32793, 32794, 32795, 32797, 32798, 32800, 32801, 32802, 32804, 32805, - 32806, 32808, 32809, 32810, 32812, 32813, 32815, 32816, 32817, 32819, 32820, 32821, 32823, 32824, 32825, 32827, - 32828, 32829, 32831, 32832, 32833, 32835, 32836, 32838, 32839, 32840, 32842, 32843, 32844, 32846, 32847, 32848, - 32850, 32851, 32852, 32854, 32855, 32856, 32858, 32859, 32860, 32862, 32863, 32864, 32866, 32867, 32868, 32870, - 32871, 32873, 32874, 32875, 32877, 32878, 32879, 32881, 32882, 32883, 32885, 32886, 32887, 32889, 32890, 32891, - 32893, 32894, 32895, 32897, 32898, 32899, 32901, 32902, 32903, 32905, 32906, 32907, 32909, 32910, 32911, 32913, - 32914, 32915, 32916, 32918, 32919, 32920, 32922, 32923, 32924, 32926, 32927, 32928, 32930, 32931, 32932, 32934, - 32935, 32936, 32938, 32939, 32940, 32942, 32943, 32944, 32946, 32947, 32948, 32949, 32951, 32952, 32953, 32955, - 32956, 32957, 32959, 32960, 32961, 32963, 32964, 32965, 32967, 32968, 32969, 32970, 32972, 32973, 32974, 32976, - 32977, 32978, 32980, 32981, 32982, 32984, 32985, 32986, 32987, 32989, 32990, 32991, 32993, 32994, 32995, 32997, - 32998, 32999, 33000, 33002, 33003, 33004, 33006, 33007, 33008, 33010, 33011, 33012, 33013, 33015, 33016, 33017, - 33019, 33020, 33021, 33022, 33024, 33025, 33026, 33028, 33029, 33030, 33031, 33033, 33034, 33035, 33037, 33038, - 33039, 33040, 33042, 33043, 33044, 33046, 33047, 33048, 33049, 33051, 33052, 33053, 33055, 33056, 33057, 33058, - 33060, 33061, 33062, 33064, 33065, 33066, 33067, 33069, 33070, 33071, 33072, 33074, 33075, 33076, 33078, 33079, - 33080, 33081, 33083, 33084, 33085, 33086, 33088, 33089, 33090, 33092, 33093, 33094, 33095, 33097, 33098, 33099, - 33100, 33102, 33103, 33104, 33106, 33107, 33108, 33109, 33111, 33112, 33113, 33114, 33116, 33117, 33118, 33119, - 33121, 33122, 33123, 33124, 33126, 33127, 33128, 33129, 33131, 33132, 33133, 33135, 33136, 33137, 33138, 33140, - 33141, 33142, 33143, 33145, 33146, 33147, 33148, 33150, 33151, 33152, 33153, 33155, 33156, 33157, 33158, 33160, - 33161, 33162, 33163, 33165, 33166, 33167, 33168, 33170, 33171, 33172, 33173, 33175, 33176, 33177, 33178, 33180, - 33181, 33182, 33183, 33184, 33186, 33187, 33188, 33189, 33191, 33192, 33193, 33194, 33196, 33197, 33198, 33199, - 33201, 33202, 33203, 33204, 33206, 33207, 33208, 33209, 33210, 33212, 33213, 33214, 33215, 33217, 33218, 33219, - 33220, 33222, 33223, 33224, 33225, 33226, 33228, 33229, 33230, 33231, 33233, 33234, 33235, 33236, 33237, 33239, - 33240, 33241, 33242, 33244, 33245, 33246, 33247, 33249, 33250, 33251, 33252, 33253, 33255, 33256, 33257, 33258, - 33259, 33261, 33262, 33263, 33264, 33266, 33267, 33268, 33269, 33270, 33272, 33273, 33274, 33275, 33277, 33278, - 33279, 33280, 33281, 33283, 33284, 33285, 33286, 33287, 33289, 33290, 33291, 33292, 33293, 33295, 33296, 33297, - 33298, 33300, 33301, 33302, 33303, 33304, 33306, 33307, 33308, 33309, 33310, 33312, 33313, 33314, 33315, 33316, - 33318, 33319, 33320, 33321, 33322, 33324, 33325, 33326, 33327, 33328, 33330, 33331, 33332, 33333, 33334, 33336, - 33337, 33338, 33339, 33340, 33342, 33343, 33344, 33345, 33346, 33348, 33349, 33350, 33351, 33352, 33353, 33355, - 33356, 33357, 33358, 33359, 33361, 33362, 33363, 33364, 33365, 33367, 33368, 33369, 33370, 33371, 33372, 33374, - 33375, 33376, 33377, 33378, 33380, 33381, 33382, 33383, 33384, 33385, 33387, 33388, 33389, 33390, 33391, 33393, - 33394, 33395, 33396, 33397, 33398, 33400, 33401, 33402, 33403, 33404, 33406, 33407, 33408, 33409, 33410, 33411, - 33413, 33414, 33415, 33416, 33417, 33418, 33420, 33421, 33422, 33423, 33424, 33425, 33427, 33428, 33429, 33430, - 33431, 33432, 33434, 33435, 33436, 33437, 33438, 33440, 33441, 33442, 33443, 33444, 33445, 33446, 33448, 33449, - 33450, 33451, 33452, 33453, 33455, 33456, 33457, 33458, 33459, 33460, 33462, 33463, 33464, 33465, 33466, 33467, - 33469, 33470, 33471, 33472, 33473, 33474, 33475, 33477, 33478, 33479, 33480, 33481, 33482, 33484, 33485, 33486, - 33487, 33488, 33489, 33490, 33492, 33493, 33494, 33495, 33496, 33497, 33499, 33500, 33501, 33502, 33503, 33504, - 33505, 33507, 33508, 33509, 33510, 33511, 33512, 33513, 33515, 33516, 33517, 33518, 33519, 33520, 33521, 33523, - 33524, 33525, 33526, 33527, 33528, 33529, 33531, 33532, 33533, 33534, 33535, 33536, 33537, 33539, 33540, 33541, - 33542, 33543, 33544, 33545, 33547, 33548, 33549, 33550, 33551, 33552, 33553, 33555, 33556, 33557, 33558, 33559, - 33560, 33561, 33562, 33564, 33565, 33566, 33567, 33568, 33569, 33570, 33571, 33573, 33574, 33575, 33576, 33577, - 33578, 33579, 33581, 33582, 33583, 33584, 33585, 33586, 33587, 33588, 33590, 33591, 33592, 33593, 33594, 33595, - 33596, 33597, 33598, 33600, 33601, 33602, 33603, 33604, 33605, 33606, 33607, 33609, 33610, 33611, 33612, 33613, - 33614, 33615, 33616, 33618, 33619, 33620, 33621, 33622, 33623, 33624, 33625, 33626, 33628, 33629, 33630, 33631, - 33632, 33633, 33634, 33635, 33636, 33638, 33639, 33640, 33641, 33642, 33643, 33644, 33645, 33646, 33648, 33649, - 33650, 33651, 33652, 33653, 33654, 33655, 33656, 33658, 33659, 33660, 33661, 33662, 33663, 33664, 33665, 33666, - 33667, 33669, 33670, 33671, 33672, 33673, 33674, 33675, 33676, 33677, 33678, 33680, 33681, 33682, 33683, 33684, - 33685, 33686, 33687, 33688, 33689, 33691, 33692, 33693, 33694, 33695, 33696, 33697, 33698, 33699, 33700, 33702, - 33703, 33704, 33705, 33706, 33707, 33708, 33709, 33710, 33711, 33712, 33714, 33715, 33716, 33717, 33718, 33719, - 33720, 33721, 33722, 33723, 33724, 33726, 33727, 33728, 33729, 33730, 33731, 33732, 33733, 33734, 33735, 33736, - 33737, 33739, 33740, 33741, 33742, 33743, 33744, 33745, 33746, 33747, 33748, 33749, 33750, 33752, 33753, 33754, - 33755, 33756, 33757, 33758, 33759, 33760, 33761, 33762, 33763, 33764, 33766, 33767, 33768, 33769, 33770, 33771, - 33772, 33773, 33774, 33775, 33776, 33777, 33778, 33780, 33781, 33782, 33783, 33784, 33785, 33786, 33787, 33788, - 33789, 33790, 33791, 33792, 33793, 33795, 33796, 33797, 33798, 33799, 33800, 33801, 33802, 33803, 33804, 33805, - 33806, 33807, 33808, 33809, 33811, 33812, 33813, 33814, 33815, 33816, 33817, 33818, 33819, 33820, 33821, 33822, - 33823, 33824, 33825, 33826, 33828, 33829, 33830, 33831, 33832, 33833, 33834, 33835, 33836, 33837, 33838, 33839, - 33840, 33841, 33842, 33843, 33844, 33846, 33847, 33848, 33849, 33850, 33851, 33852, 33853, 33854, 33855, 33856, - 33857, 33858, 33859, 33860, 33861, 33862, 33863, 33865, 33866, 33867, 33868, 33869, 33870, 33871, 33872, 33873, - 33874, 33875, 33876, 33877, 33878, 33879, 33880, 33881, 33882, 33883, 33884, 33885, 33887, 33888, 33889, 33890, - 33891, 33892, 33893, 33894, 33895, 33896, 33897, 33898, 33899, 33900, 33901, 33902, 33903, 33904, 33905, 33906, - 33907, 33908, 33909, 33911, 33912, 33913, 33914, 33915, 33916, 33917, 33918, 33919, 33920, 33921, 33922, 33923, - 33924, 33925, 33926, 33927, 33928, 33929, 33930, 33931, 33932, 33933, 33934, 33935, 33936, 33937, 33938, 33940, - 33941, 33942, 33943, 33944, 33945, 33946, 33947, 33948, 33949, 33950, 33951, 33952, 33953, 33954, 33955, 33956, - 33957, 33958, 33959, 33960, 33961, 33962, 33963, 33964, 33965, 33966, 33967, 33968, 33969, 33970, 33971, 33972, - 33973, 33974, 33975, 33977, 33978, 33979, 33980, 33981, 33982, 33983, 33984, 33985, 33986, 33987, 33988, 33989, - 33990, 33991, 33992, 33993, 33994, 33995, 33996, 33997, 33998, 33999, 34000, 34001, 34002, 34003, 34004, 34005, - 34006, 34007, 34008, 34009, 34010, 34011, 34012, 34013, 34014, 34015, 34016, 34017, 34018, 34019, 34020, 34021, - 34022, 34023, 34024, 34025, 34026, 34027, 34028, 34029, 34030, 34031, 34032, 34033, 34034, 34035, 34036, 34037, - 34038, 34039, 34040, 34041, 34042, 34043, 34044, 34045, 34047, 34048, 34049, 34050, 34051, 34052, 34053, 34054, - 34055, 34056, 34057, 34058, 34059, 34060, 34061, 34062, 34063, 34064, 34065, 34066, 34067, 34068, 34069, 34070, - 34071 -}; -static_assert(NUMBER_OF_TRANSACTIONS_PER_TICK == 4096, "TxRevenuePoints expect of 4096 total transactions per tick"); +static constexpr unsigned short gTxRevenuePoints[1 + 1024] = { 0, 710, 1125, 1420, 1648, 1835, 1993, 2129, 2250, 2358, 2455, 2545, 2627, 2702, 2773, 2839, 2901, 2960, 3015, 3068, 3118, 3165, 3211, 3254, 3296, 3336, 3375, 3412, 3448, 3483, 3516, 3549, 3580, 3611, 3641, 3670, 3698, 3725, 3751, 3777, 3803, 3827, 3851, 3875, 3898, 3921, 3943, 3964, 3985, 4006, 4026, 4046, 4066, 4085, 4104, 4122, 4140, 4158, 4175, 4193, 4210, 4226, 4243, 4259, 4275, 4290, 4306, 4321, 4336, 4350, 4365, 4379, 4393, 4407, 4421, 4435, 4448, 4461, 4474, 4487, 4500, 4512, 4525, 4537, 4549, 4561, 4573, 4585, 4596, 4608, 4619, 4630, 4641, 4652, 4663, 4674, 4685, 4695, 4705, 4716, 4726, 4736, 4746, 4756, 4766, 4775, 4785, 4795, 4804, 4813, 4823, 4832, 4841, 4850, 4859, 4868, 4876, 4885, 4894, 4902, 4911, 4919, 4928, 4936, 4944, 4952, 4960, 4968, 4976, 4984, 4992, 5000, 5008, 5015, 5023, 5031, 5038, 5046, 5053, 5060, 5068, 5075, 5082, 5089, 5096, 5103, 5110, 5117, 5124, 5131, 5138, 5144, 5151, 5158, 5164, 5171, 5178, 5184, 5191, 5197, 5203, 5210, 5216, 5222, 5228, 5235, 5241, 5247, 5253, 5259, 5265, 5271, 5277, 5283, 5289, 5295, 5300, 5306, 5312, 5318, 5323, 5329, 5335, 5340, 5346, 5351, 5357, 5362, 5368, 5373, 5378, 5384, 5389, 5394, 5400, 5405, 5410, 5415, 5420, 5425, 5431, 5436, 5441, 5446, 5451, 5456, 5461, 5466, 5471, 5475, 5480, 5485, 5490, 5495, 5500, 5504, 5509, 5514, 5518, 5523, 5528, 5532, 5537, 5542, 5546, 5551, 5555, 5560, 5564, 5569, 5573, 5577, 5582, 5586, 5591, 5595, 5599, 5604, 5608, 5612, 5616, 5621, 5625, 5629, 5633, 5637, 5642, 5646, 5650, 5654, 5658, 5662, 5666, 5670, 5674, 5678, 5682, 5686, 5690, 5694, 5698, 5702, 5706, 5710, 5714, 5718, 5721, 5725, 5729, 5733, 5737, 5740, 5744, 5748, 5752, 5755, 5759, 5763, 5766, 5770, 5774, 5777, 5781, 5785, 5788, 5792, 5795, 5799, 5802, 5806, 5809, 5813, 5816, 5820, 5823, 5827, 5830, 5834, 5837, 5841, 5844, 5847, 5851, 5854, 5858, 5861, 5864, 5868, 5871, 5874, 5878, 5881, 5884, 5887, 5891, 5894, 5897, 5900, 5904, 5907, 5910, 5913, 5916, 5919, 5923, 5926, 5929, 5932, 5935, 5938, 5941, 5944, 5948, 5951, 5954, 5957, 5960, 5963, 5966, 5969, 5972, 5975, 5978, 5981, 5984, 5987, 5990, 5993, 5996, 5999, 6001, 6004, 6007, 6010, 6013, 6016, 6019, 6022, 6025, 6027, 6030, 6033, 6036, 6039, 6041, 6044, 6047, 6050, 6053, 6055, 6058, 6061, 6064, 6066, 6069, 6072, 6075, 6077, 6080, 6083, 6085, 6088, 6091, 6093, 6096, 6099, 6101, 6104, 6107, 6109, 6112, 6115, 6117, 6120, 6122, 6125, 6128, 6130, 6133, 6135, 6138, 6140, 6143, 6145, 6148, 6151, 6153, 6156, 6158, 6161, 6163, 6166, 6168, 6170, 6173, 6175, 6178, 6180, 6183, 6185, 6188, 6190, 6193, 6195, 6197, 6200, 6202, 6205, 6207, 6209, 6212, 6214, 6216, 6219, 6221, 6224, 6226, 6228, 6231, 6233, 6235, 6238, 6240, 6242, 6244, 6247, 6249, 6251, 6254, 6256, 6258, 6260, 6263, 6265, 6267, 6269, 6272, 6274, 6276, 6278, 6281, 6283, 6285, 6287, 6289, 6292, 6294, 6296, 6298, 6300, 6303, 6305, 6307, 6309, 6311, 6313, 6316, 6318, 6320, 6322, 6324, 6326, 6328, 6330, 6333, 6335, 6337, 6339, 6341, 6343, 6345, 6347, 6349, 6351, 6353, 6356, 6358, 6360, 6362, 6364, 6366, 6368, 6370, 6372, 6374, 6376, 6378, 6380, 6382, 6384, 6386, 6388, 6390, 6392, 6394, 6396, 6398, 6400, 6402, 6404, 6406, 6408, 6410, 6412, 6414, 6416, 6418, 6420, 6421, 6423, 6425, 6427, 6429, 6431, 6433, 6435, 6437, 6439, 6441, 6443, 6444, 6446, 6448, 6450, 6452, 6454, 6456, 6458, 6459, 6461, 6463, 6465, 6467, 6469, 6471, 6472, 6474, 6476, 6478, 6480, 6482, 6483, 6485, 6487, 6489, 6491, 6493, 6494, 6496, 6498, 6500, 6502, 6503, 6505, 6507, 6509, 6510, 6512, 6514, 6516, 6518, 6519, 6521, 6523, 6525, 6526, 6528, 6530, 6532, 6533, 6535, 6537, 6538, 6540, 6542, 6544, 6545, 6547, 6549, 6550, 6552, 6554, 6556, 6557, 6559, 6561, 6562, 6564, 6566, 6567, 6569, 6571, 6572, 6574, 6576, 6577, 6579, 6581, 6582, 6584, 6586, 6587, 6589, 6591, 6592, 6594, 6596, 6597, 6599, 6600, 6602, 6604, 6605, 6607, 6609, 6610, 6612, 6613, 6615, 6617, 6618, 6620, 6621, 6623, 6625, 6626, 6628, 6629, 6631, 6632, 6634, 6636, 6637, 6639, 6640, 6642, 6643, 6645, 6647, 6648, 6650, 6651, 6653, 6654, 6656, 6657, 6659, 6660, 6662, 6663, 6665, 6667, 6668, 6670, 6671, 6673, 6674, 6676, 6677, 6679, 6680, 6682, 6683, 6685, 6686, 6688, 6689, 6691, 6692, 6694, 6695, 6697, 6698, 6699, 6701, 6702, 6704, 6705, 6707, 6708, 6710, 6711, 6713, 6714, 6716, 6717, 6718, 6720, 6721, 6723, 6724, 6726, 6727, 6729, 6730, 6731, 6733, 6734, 6736, 6737, 6739, 6740, 6741, 6743, 6744, 6746, 6747, 6748, 6750, 6751, 6753, 6754, 6755, 6757, 6758, 6760, 6761, 6762, 6764, 6765, 6767, 6768, 6769, 6771, 6772, 6773, 6775, 6776, 6778, 6779, 6780, 6782, 6783, 6784, 6786, 6787, 6788, 6790, 6791, 6793, 6794, 6795, 6797, 6798, 6799, 6801, 6802, 6803, 6805, 6806, 6807, 6809, 6810, 6811, 6813, 6814, 6815, 6816, 6818, 6819, 6820, 6822, 6823, 6824, 6826, 6827, 6828, 6830, 6831, 6832, 6833, 6835, 6836, 6837, 6839, 6840, 6841, 6842, 6844, 6845, 6846, 6848, 6849, 6850, 6851, 6853, 6854, 6855, 6856, 6858, 6859, 6860, 6862, 6863, 6864, 6865, 6867, 6868, 6869, 6870, 6872, 6873, 6874, 6875, 6877, 6878, 6879, 6880, 6882, 6883, 6884, 6885, 6886, 6888, 6889, 6890, 6891, 6893, 6894, 6895, 6896, 6897, 6899, 6900, 6901, 6902, 6904, 6905, 6906, 6907, 6908, 6910, 6911, 6912, 6913, 6914, 6916, 6917, 6918, 6919, 6920, 6921, 6923, 6924, 6925, 6926, 6927, 6929, 6930, 6931, 6932, 6933, 6934, 6936, 6937, 6938, 6939, 6940, 6941, 6943, 6944, 6945, 6946, 6947, 6948, 6950, 6951, 6952, 6953, 6954, 6955, 6957, 6958, 6959, 6960, 6961, 6962, 6963, 6965, 6966, 6967, 6968, 6969, 6970, 6971, 6972, 6974, 6975, 6976, 6977, 6978, 6979, 6980, 6981, 6983, 6984, 6985, 6986, 6987, 6988, 6989, 6990, 6991, 6993, 6994, 6995, 6996, 6997, 6998, 6999, 7000, 7001, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010, 7011, 7012, 7013, 7015, 7016, 7017, 7018, 7019, 7020, 7021, 7022, 7023, 7024, 7025, 7026, 7027, 7029, 7030, 7031, 7032, 7033, 7034, 7035, 7036, 7037, 7038, 7039, 7040, 7041, 7042, 7043, 7044, 7046, 7047, 7048, 7049, 7050, 7051, 7052, 7053, 7054, 7055, 7056, 7057, 7058, 7059, 7060, 7061, 7062, 7063, 7064, 7065, 7066, 7067, 7068, 7069, 7070, 7071, 7073, 7074, 7075, 7076, 7077, 7078, 7079, 7080, 7081, 7082, 7083, 7084, 7085, 7086, 7087, 7088, 7089, 7090, 7091, 7092, 7093, 7094, 7095, 7096, 7097, 7098, 7099 }; + +static_assert(NUMBER_OF_TRANSACTIONS_PER_TICK == 1024, "TxRevenuePoints expect of 1024 total transactions per tick"); static_assert(gTxRevenuePoints[NUMBER_OF_TRANSACTIONS_PER_TICK] > gTxRevenuePoints[NUMBER_OF_TRANSACTIONS_PER_TICK - 1], "gTxRevenuePoints tail not monotonic (missing entries)"); @@ -839,4 +579,3 @@ static void computeMultiDimRevenue() gMultiDimRevenue.revenue[i] = (long long)(num / (REVENUE_SCALE * REVENUE_SCALE * REVENUE_SCALE)); } } - From 83bbf748c737563e3b566ab50316531f67555944 Mon Sep 17 00:00:00 2001 From: Jean Date: Wed, 10 Jun 2026 14:55:23 +0200 Subject: [PATCH 28/28] =?UTF-8?q?feat(QSB):=20multisig=20governance=20?= =?UTF-8?q?=E2=80=94=20real=20admin=20keys,=20testnet=20constructionEpoch,?= =?UTF-8?q?=20test=20isolation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - INITIALIZE now sets real deployment keys (admin-1 and admin-2 from oracle/.temp/) instead of placeholder id(100,200,300,400) literals - QSB constructionEpoch updated to 216 so INITIALIZE fires on a clean local testnet reset (epoch 216 = testnet starting epoch) - contract_qsb.cpp constructor overrides admins back to stable test identities (ADMIN/ADMIN2) after calling INITIALIZE, so unit tests remain independent of the live deployment keys; removed two strict admin identity checks that depended on the old placeholder values Co-Authored-By: Claude Sonnet 4.6 --- src/contract_core/contract_def.h | 2 +- src/contracts/QubicSolanaBridge.h | 10 +++++----- test/contract_qsb.cpp | 15 ++++++++++----- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 4bc159a5..b38b3244 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -423,7 +423,7 @@ constexpr struct ContractDescription {"QUSINO", 208, 10000, sizeof(QUSINO::StateData)}, // proposal in epoch 206, IPO in 207, construction and first use in 208 {"ESCROW", 210, 10000, sizeof(ESCROW::StateData)}, // proposal in epoch 208, IPO in 209, construction and first use in 210 {"GGWP", 217, 10000, sizeof(WOLFPACK::StateData)}, // proposal in epoch 215, IPO in 216, construction and first use in 217 - {"QSB", 212, 10000, sizeof(QSB::StateData)}, // local testnet: constructionEpoch <= testnet epoch to skip IPO + {"QSB", 216, 10000, sizeof(QSB::StateData)}, // local testnet: align constructionEpoch with current testnet epoch so INITIALIZE runs on clean reset // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA::StateData)}, diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h index b007233e..07313257 100644 --- a/src/contracts/QubicSolanaBridge.h +++ b/src/contracts/QubicSolanaBridge.h @@ -2177,12 +2177,12 @@ struct QSB : public ContractBase INITIALIZE() { // Multisig admin setup — 2-of-2 from deployment. - // Replace both keys with real production keys before mainnet deployment. - // Admin 0: id(100, 200, 300, 400) — test key, matches ADMIN in contract_qsb.cpp - // Admin 1: id(101, 201, 301, 401) — test key, matches ADMIN2 in contract_qsb.cpp + // Keys correspond to oracle/.temp/qubic-admin.keys.json (slot 0) + // and oracle/.temp/qubic-admin-2.keys.json (slot 1). + // oracle/.temp/qubic-admin-3.keys.json is added via proposal during QA. setMemory(state.mut().admins, 0); - state.mut().admins.set(0, id(100ULL, 200ULL, 300ULL, 400ULL)); - state.mut().admins.set(1, id(101ULL, 201ULL, 301ULL, 401ULL)); + state.mut().admins.set(0, id(11994886480163374182ULL, 7222723150474050185ULL, 4187743050690849231ULL, 4967671197750064684ULL)); + state.mut().admins.set(1, id(1491071035376662822ULL, 7392187382213737082ULL, 14591294638558326625ULL, 12799863271090897602ULL)); state.mut().adminCount = 2; state.mut().adminThreshold = 2; setMemory(state.mut().proposals, 0); diff --git a/test/contract_qsb.cpp b/test/contract_qsb.cpp index 52beca5d..fcf0f235 100644 --- a/test/contract_qsb.cpp +++ b/test/contract_qsb.cpp @@ -122,7 +122,16 @@ class ContractTestingQSB : protected ContractTesting initEmptyUniverse(); INIT_CONTRACT(QSB); callSystemProcedure(QSB_CONTRACT_INDEX, INITIALIZE); - // INITIALIZE sets ADMIN (slot 0) and ADMIN2 (slot 1) with threshold=2. + // Override admins to use stable test identities (ADMIN/ADMIN2) so all test + // logic remains independent of the live-deployment keys in INITIALIZE. + { + auto* s = (QSB::StateData*)contractStates[QSB_CONTRACT_INDEX]; + setMemory(s->admins, 0); + s->admins.set(0, ADMIN); + s->admins.set(1, ADMIN2); + s->adminCount = 2; + s->adminThreshold = 2; + } checkContractExecCleanup(); } @@ -570,8 +579,6 @@ TEST(ContractTestingQSB, TestGetConfig_ReturnsInitialState) EXPECT_EQ(config.adminCount, 2); EXPECT_EQ(config.adminThreshold, 2); - EXPECT_EQ(config.admins.get(0), ADMIN); - EXPECT_EQ(config.admins.get(1), ADMIN2); EXPECT_EQ(config.protocolFeeRecipient, NULL_ID); EXPECT_EQ(config.oracleFeeRecipient, NULL_ID); EXPECT_EQ(config.bpsFee, 0u); @@ -966,8 +973,6 @@ TEST(ContractTestingQSB, TestInitialization) // Check initial state test.getState()->checkAdminCount(2); test.getState()->checkAdminThreshold(2); - test.getState()->checkIsAdmin(ADMIN); - test.getState()->checkIsAdmin(ADMIN2); test.getState()->checkPaused(false); test.getState()->checkOracleThreshold(67); // Default 67% test.getState()->checkOracleCount(0);