diff --git a/.gitignore b/.gitignore index 28bcf5fa..755559ec 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ x64/ .clang-format tmp build/ +build-test/ /build** # Build directories and temporary files @@ -31,4 +32,4 @@ CLAUDE.md # Explorer embedded assets (generated by regen.sh at build) src/extensions/http/controller/explorer_assets.generated.h -crash.dump \ No newline at end of file +crash.dump 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/docker/orchestrator/Dockerfile b/docker/orchestrator/Dockerfile index 1a070328..d359954d 100644 --- a/docker/orchestrator/Dockerfile +++ b/docker/orchestrator/Dockerfile @@ -41,7 +41,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/Qubic.vcxproj b/src/Qubic.vcxproj index 02120a0e..dd947bd3 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -50,6 +50,7 @@ + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index cfcb739a..f567e113 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -320,6 +320,10 @@ contracts + + + contracts + contracts diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index f6f11909..b38b3244 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -282,25 +282,31 @@ #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" - -#ifndef NO_GGWP - -#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" - -#endif +#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 29 +#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 @@ -413,13 +419,12 @@ 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 -#ifndef NO_GGWP - {"GGWP", 217, 10000, sizeof(WOLFPACK::StateData)}, // proposal in epoch 215, IPO in 216, construction and first use in 217 -#endif - // new contracts should be added above this line + {"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", 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)}, {"TESTEXB", 138, 10000, sizeof(TESTEXB::StateData)}, @@ -539,13 +544,12 @@ 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); -#ifndef NO_GGWP - REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(WOLFPACK); -#endif - // new contracts should be added above this line + 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 REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXB); diff --git a/src/contracts/QubicSolanaBridge.h b/src/contracts/QubicSolanaBridge.h new file mode 100644 index 00000000..07313257 --- /dev/null +++ b/src/contracts/QubicSolanaBridge.h @@ -0,0 +1,2211 @@ +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 = 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 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). +struct QSBOrderMessage +{ + uint32 protocolNameLen; // 0: always 11 + Array protocolName; // 4: QubicBridge (11 used, 5 zero-padded) + uint32 protocolVersionLen; // 20: always 1 + Array protocolVersion; // 24: version byte (49 = ASCII '1') + Array contractAddress; // 25: destination contract address (LE-padded index) + uint32 networkIn; // 57 + uint32 networkOut; // 61 + Array tokenIn; // 65 + Array tokenOut; // 97 + Array fromAddress; // 129 + Array toAddress; // 161 + uint64 amount; // 193 + uint64 relayerFee; // 201 + Array nonce; // 209 + uint32 orderEra; // 241 +}; +static constexpr uint32 QSB_QUERY_MAX_PAGE_SIZE = 64; // max entries per paginated query + +// Log types for QSB contract (no enums allowed in contracts) +static constexpr uint32 QSBLogLock = 1; +static constexpr uint32 QSBLogOverrideLock = 2; +static constexpr uint32 QSBLogUnlock = 3; +static constexpr uint32 QSBLogPaused = 4; +static constexpr uint32 QSBLogUnpaused = 5; +static constexpr uint32 QSBLogAdminTransferred = 6; +static constexpr uint32 QSBLogThresholdUpdated = 7; +static constexpr uint32 QSBLogRoleGranted = 8; +static constexpr uint32 QSBLogRoleRevoked = 9; +static constexpr uint32 QSBLogFeeParametersUpdated = 10; +static constexpr uint32 QSBLogProposalCreated = 11; +static constexpr uint32 QSBLogProposalApproved = 12; +static constexpr uint32 QSBLogProposalExecuted = 13; +static constexpr uint32 QSBLogProposalCancelled = 14; + +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 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 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; +static constexpr uint8 QSBReasonInvalidAdmin = 21; +static constexpr uint8 QSBReasonInvalidRole = 22; +static constexpr uint8 QSBReasonOrderNotFound = 23; +static constexpr uint8 QSBReasonOverrideLimitReached = 24; +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 +{ +}; + +struct QSB : public ContractBase +{ +public: + 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; + }; + + typedef Array OrderHash; + + // Signature wrapper compatible with QPI::signatureValidity + struct SignatureData + { + id signer; // oracle id (public key) + Array signature; // raw 64-byte signature + }; + + struct FilledOrderEntry + { + OrderHash hash; + bit used; + }; + + struct RoleEntry + { + id account; + bit active; + }; + + struct LockedOrderEntry + { + id sender; + uint64 amount; + uint64 relayerFee; + uint32 networkOut; + uint32 nonce; + Array toAddress; + OrderHash orderHash; + uint32 lockEpoch; + uint32 orderEra; + bit active; + uint8 overrideLockCount; // at +161; 6 bytes padding follow to keep struct at 168 bytes + }; + + 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; + }; + + 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) + + 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 + // --------------------------------------------------------------------- + + struct Lock_input + { + uint64 amount; + uint64 relayerFee; + Array toAddress; + uint32 networkOut; + uint32 nonce; + }; + + struct Lock_output + { + OrderHash orderHash; + bit success; + }; + + struct OverrideLock_input + { + Array toAddress; + uint64 relayerFee; + uint32 nonce; + }; + + struct OverrideLock_output + { + OrderHash orderHash; + bit success; + }; + + struct Unlock_input + { + Order order; + uint32 numSignatures; + Array signatures; + }; + + struct Unlock_output + { + OrderHash orderHash; + bit success; + }; + + struct TransferAdmin_input + { + id newAdmin; + }; + + struct TransferAdmin_output + { + bit success; + }; + + struct EditOracleThreshold_input + { + uint8 newThreshold; + }; + + struct EditOracleThreshold_output + { + uint8 oldThreshold; + bit success; + }; + + struct AddRole_input + { + id account; + uint8 role; // see Role enum + }; + + struct AddRole_output + { + bit success; + }; + + struct RemoveRole_input + { + id account; + uint8 role; + }; + + struct RemoveRole_output + { + bit success; + }; + + struct Pause_input + { + }; + + struct Pause_output + { + bit success; + }; + + typedef Pause_input Unpause_input; + typedef Pause_output Unpause_output; + + 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; + }; + + 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 + // --------------------------------------------------------------------- + + struct GetConfig_input + { + }; + + struct GetConfig_output + { + uint8 adminCount; + uint8 adminThreshold; + Array admins; + 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; + }; + + struct ComputeOrderHash_input + { + Order order; + }; + + struct ComputeOrderHash_output + { + OrderHash hash; + }; + + struct GetOracles_input + { + }; + + struct GetOracles_output + { + uint32 count; + Array accounts; + }; + + struct GetPausers_input + { + }; + + struct GetPausers_output + { + uint32 count; + Array accounts; + }; + + 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; + }; + + 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 + { + 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; + Array filledOrdersPrev; + Array lockedOrders; + uint32 lastFilledOrdersNextOverwriteIdx; + uint32 lastLockedOrdersNextOverwriteIdx; + uint32 oracleCount; + uint32 pauserCount; + uint32 bpsFee; + uint32 protocolFee; + uint8 oracleThreshold; // percent [1..100] + bit paused; + uint32 orderEra; + }; + +protected: + // --------------------------------------------------------------------- + // Procedure result structs + // --------------------------------------------------------------------- + + struct LockResult + { + bit success; + uint8 reasonCode; + OrderHash orderHash; + uint32 orderEra; + }; + + struct OverrideLockResult + { + 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; + }; + +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); + } + + 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); + 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 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) + { + 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; + } + output.totalActive = locals.totalActive; + output.returned = locals.collected; + } + + struct GetFilledOrders_locals + { + 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) + { + 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; + } + + // --------------------------------------------------------------------- + // Admin procedures (multisig) + // --------------------------------------------------------------------- + + struct Propose_locals + { + ProposeResult result; + QSBLogProposalMessage logMsg; + }; + + 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; + + 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 ApproveProposal_locals + { + ApproveProposalResult result; + QSBLogProposalMessage logMsg; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(ApproveProposal) + { + 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 = 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 + { + CancelProposalResult result; + QSBLogProposalMessage logMsg; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(CancelProposal) + { + 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.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) + { + 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 = output.success ? 1 : 0; + locals.logMsg.reasonCode = locals.result.reasonCode; + locals.logMsg._terminator = 0; + LOG_INFO(locals.logMsg); + } + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + 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); + + REGISTER_USER_PROCEDURE(Lock, 1); + REGISTER_USER_PROCEDURE(OverrideLock, 2); + REGISTER_USER_PROCEDURE(Unlock, 3); + + REGISTER_USER_PROCEDURE(Pause, 14); + + REGISTER_USER_PROCEDURE(Propose, 20); + REGISTER_USER_PROCEDURE(ApproveProposal, 21); + REGISTER_USER_PROCEDURE(CancelProposal, 22); + } + +protected: + // --------------------------------------------------------------------- + // Low-level helpers + // --------------------------------------------------------------------- + + inline static void digestToOrderHash(const id &digest, OrderHash &outHash) + { + outHash.setMem(digest); + } + + inline static void initDomainPrefix(QSBOrderMessage &msg) + { + setMemory(msg, 0); + msg.protocolNameLen = 11; + msg.protocolName.set(0, 81); // Q + msg.protocolName.set(1, 117); // u + msg.protocolName.set(2, 98); // b + msg.protocolName.set(3, 105); // i + msg.protocolName.set(4, 99); // c + msg.protocolName.set(5, 66); // B + msg.protocolName.set(6, 114); // r + msg.protocolName.set(7, 105); // i + msg.protocolName.set(8, 100); // d + msg.protocolName.set(9, 103); // g + msg.protocolName.set(10, 101); // e + msg.protocolVersionLen = 1; + msg.protocolVersion.set(0, 49); // 1 + msg.contractAddress.set(0, (uint8)(CONTRACT_INDEX & 0xFF)); + msg.contractAddress.set(1, (uint8)((CONTRACT_INDEX >> 8) & 0xFF)); + } + + inline static void buildOrderMessage( + QSBOrderMessage &msg, + const Order &order, + OrderHash &tmpIdBytes) + { + uint32 i; + initDomainPrefix(msg); + msg.networkIn = order.networkIn; + msg.networkOut = order.networkOut; + for (i = 0; i < 32; ++i) + msg.tokenIn.set(i, order.tokenIn.get(i)); + for (i = 0; i < 32; ++i) + msg.tokenOut.set(i, order.tokenOut.get(i)); + tmpIdBytes.setMem(order.fromAddress); + for (i = 0; i < 32; ++i) + msg.fromAddress.set(i, tmpIdBytes.get(i)); + tmpIdBytes.setMem(order.toAddress); + for (i = 0; i < 32; ++i) + msg.toAddress.set(i, tmpIdBytes.get(i)); + msg.amount = order.amount; + msg.relayerFee = order.relayerFee; + for (i = 0; i < 32; ++i) + msg.nonce.set(i, order.nonce.get(i)); + msg.orderEra = order.orderEra; + } + + inline static uint8 countBitsUint8(uint8 mask) + { + uint8 count = 0; + for (uint8 i = 0; i < 8; ++i) + { + if (mask & (uint8)(1u << i)) + ++count; + } + return count; + } + + inline static bool isAdmin(const QPI::ContractState &state, const id &who) + { + 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; + } + + inline static sint64 findAdminIndex(const QPI::ContractState &state, const id &who) + { + 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; + } + + 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) + { + if (state.get().pausers.get(i).active && state.get().pausers.get(i).account == who) + return true; + } + return false; + } + + inline static void cancelAllPendingProposals(QPI::ContractState &state) + { + AdminProposal prop; + for (uint32 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); + } + } + } + + 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); + 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; + } + + inline static sint64 findOracleIndex(const QPI::ContractState &state, const id &account) + { + 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; + } + + inline static sint64 findPauserIndex(const QPI::ContractState &state, const id &account) + { + 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; + } + + 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) + { + 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; + } + } + + 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; + } + } + + // 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) + { + if (entry.hash.get(j) != hash.get(j)) + { + same = false; + break; + } + } + 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; + } + + inline static sint64 findLockedOrderIndexByNonce(const QPI::ContractState &state, uint32 nonce) + { + 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; + } + return NULL_INDEX; + } + + 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); + } + } + } + + // --------------------------------------------------------------------- + // 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) + { + 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); + + result.success = true; + return result; + } + + inline static OverrideLockResult tryOverrideLock( + const QPI::QpiContextProcedureCall &qpi, + QPI::ContractState &state, + const OverrideLock_input &input) + { + 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; + } + + LockedOrderEntry entry = state.get().lockedOrders.get((uint32)idx); + + if (entry.sender != qpi.invocator()) + { + result.reasonCode = QSBReasonNotSender; + return result; + } + + if (entry.overrideLockCount >= QSB_OVERRIDE_LOCK_MAX_ATTEMPTS) + { + result.reasonCode = QSBReasonOverrideLimitReached; + return result; + } + + if (input.relayerFee >= entry.amount) + { + result.reasonCode = QSBReasonBadRelayerFee; + return result; + } + + copyMemory(entry.toAddress, input.toAddress); + entry.relayerFee = input.relayerFee; + + 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 UnlockResult tryUnlock( + const QPI::QpiContextProcedureCall &qpi, + QPI::ContractState &state, + const Unlock_input &input) + { + 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 (input.order.amount == 0 || input.order.relayerFee >= input.order.amount) + { + 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; + } + + // 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; + } + + // 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)) + { + result.reasonCode = QSBReasonAlreadyFilled; + return result; + } + + if (state.get().oracleCount == 0 || input.numSignatures == 0) + { + result.reasonCode = QSBReasonNoOracles; + return result; + } + + // 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) + { + 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) + { + 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; + } + + 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; + + tmpMul = uint128(bpsFeeAmount) * uint128(state.get().protocolFee); + tmpMul2 = div(tmpMul, uint128(100)); + uint64 protocolFeeAmount = (uint64)tmpMul2.low; + + uint64 oracleFeeAmount = (bpsFeeAmount >= protocolFeeAmount) ? bpsFeeAmount - protocolFeeAmount : 0; + + 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; + + 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; + } + + 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; + } + + + // --------------------------------------------------------------------- + // Epoch processing + // --------------------------------------------------------------------- + + struct END_EPOCH_locals {}; + + END_EPOCH_WITH_LOCALS() + { + sweepExpiredProposals(qpi, state); + } + + // --------------------------------------------------------------------- + // Initialization + // --------------------------------------------------------------------- + + INITIALIZE() + { + // Multisig admin setup — 2-of-2 from deployment. + // 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(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); + + state.mut().paused = false; + + 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); + setMemory(state.mut().filledOrders, 0); + setMemory(state.mut().filledOrdersPrev, 0); + setMemory(state.mut().lockedOrders, 0); + + 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/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/private_settings.h b/src/private_settings.h index c27c245c..afe99fe2 100644 --- a/src/private_settings.h +++ b/src/private_settings.h @@ -1371,7 +1371,18 @@ 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) + "zvkkqymkeiblorlfufwgkqourvtoxtxaomjxheggflxuayywhywfabm", // jean wallet (BYUYXZKAENFJCCXMACJVENKVKDMAEASMMWSKKRRVHANCCNKPYDPQNROCQKHA) }; // number of private ips for computor's internal services @@ -1381,7 +1392,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/public_settings.h b/src/public_settings.h index 4ca4fe4c..b12d8b9f 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -161,10 +161,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 3000ULL -#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 7331cddd..4a619bc3 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -33,10 +33,12 @@ // Uncomment to enable. // ---------------------------------------------------------------------------- -// #define TESTNET -// #define TESTNET_PREFILL_QUS -// #define TESTNET_LITE_RAM -#define USE_SWAP +#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 +// UNCOMMENT this line to enable it +#define USE_SWAP // ============================================================================ @@ -64,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 @@ -377,7 +377,7 @@ static bool loadContractStateFiles(CHAR16* directory = NULL, bool forceLoadFromF static bool loadContractExecFeeFiles(CHAR16* directory = NULL, bool loadAccumulatedTime = false); #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 @@ -533,20 +533,20 @@ 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; -#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() -{ -} +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() +{ +} // 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) @@ -3272,22 +3272,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 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 - } - else - { - // it should never go here + #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 + } + else + { + // it should never go here } // Ensure to only call INITIALIZE and BEGIN_EPOCH once per epoch: @@ -4530,10 +4530,10 @@ static void endEpoch() } -#if !START_NETWORK_FROM_SCRATCH - -static bool haveSamePrevDigestsAndTime(const Tick& A, const Tick& B) -{ +#if !START_NETWORK_FROM_SCRATCH + +static bool haveSamePrevDigestsAndTime(const Tick& A, const Tick& B) +{ return A.prevComputerDigest == B.prevComputerDigest && A.prevResourceTestingDigest == B.prevResourceTestingDigest && A.prevTransactionBodyDigest == B.prevTransactionBodyDigest && @@ -4553,9 +4553,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); @@ -4631,12 +4631,12 @@ static void initializeFirstTick() } } } - _mm_pause(); - } -} -#endif - -#if TICK_STORAGE_AUTOSAVE_MODE + _mm_pause(); + } +} +#endif + +#if TICK_STORAGE_AUTOSAVE_MODE // Invalid snapshot data static bool invalidateNodeStates(CHAR16* directory) @@ -6185,15 +6185,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; +#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; while (!shutDownNode) { @@ -7366,19 +7366,19 @@ 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 - setContractFeeReserve(TESTEXA_CONTRACT_INDEX, 100000000000); - setContractFeeReserve(TESTEXB_CONTRACT_INDEX, 100000000000); - setContractFeeReserve(TESTEXC_CONTRACT_INDEX, 100000000000); + } + 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 + setContractFeeReserve(TESTEXA_CONTRACT_INDEX, 100000000000); + setContractFeeReserve(TESTEXB_CONTRACT_INDEX, 100000000000); + setContractFeeReserve(TESTEXC_CONTRACT_INDEX, 100000000000); setContractFeeReserve(TESTEXD_CONTRACT_INDEX, 100000000000); #endif @@ -7443,6 +7443,11 @@ static bool initialize() 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(); 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)); } } - diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bc87d9d8..37b02161 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -30,11 +30,13 @@ 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 contract_qbond.cpp + contract_qsb.cpp contract_qearn.cpp contract_qswap.cpp contract_qutil.cpp diff --git a/test/contract_qsb.cpp b/test/contract_qsb.cpp new file mode 100644 index 00000000..fcf0f235 --- /dev/null +++ b/test/contract_qsb.cpp @@ -0,0 +1,2134 @@ +#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 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); +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 checkAdminCount(uint8 expected) const + { + 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 + { + 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) + { + markOrderFilled(asMutState(), hash); + } + + // 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 +{ +public: + ContractTestingQSB() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(QSB); + callSystemProcedure(QSB_CONTRACT_INDEX, INITIALIZE); + // 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(); + } + + ~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; + } + + // 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() + { + 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; + 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; + } + + // ============================================================================ + // Admin Procedure Helpers (proposal system) + // ============================================================================ + + QSB::Pause_output pause(const id& user) + { + QSB::Pause_input input; + QSB::Pause_output output; + increaseEnergy(user, 1); + invokeUserProcedure(QSB_CONTRACT_INDEX, 14, input, output, user, 0); + return output; + } + + QSB::Propose_output propose(const id& user, const QSB::Propose_input& input) + { + QSB::Propose_output output; + increaseEnergy(user, 1); + invokeUserProcedure(QSB_CONTRACT_INDEX, 20, input, output, user, 0); + return output; + } + + QSB::ApproveProposal_output approveProposal(const id& user, uint8 proposalId) + { + 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::CancelProposal_output cancelProposal(const id& user, uint8 proposalId) + { + 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; + } + + // 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::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::ApproveProposal_output proposeRemoveRole(const id& proposer, const id& approver, uint8 role, const id& account) + { + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropRemoveRole; + input.role = role; + input.targetId = account; + return proposeAndApprove(proposer, approver, input); + } + + QSB::ApproveProposal_output proposeEditOracleThreshold(const id& proposer, const id& approver, uint8 newThreshold) + { + 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; + 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 + // ============================================================================ + + 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; + } + + 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; + } +}; + +// ============================================================================ +// View helper function tests (GetConfig, IsOracle, IsPauser, GetLockedOrder, IsOrderFilled) +// ============================================================================ + +TEST(ContractTestingQSB, TestGetConfig_ReturnsInitialState) +{ + ContractTestingQSB test; + + QSB::GetConfig_output config = test.getConfig(); + + EXPECT_EQ(config.adminCount, 2); + EXPECT_EQ(config.adminThreshold, 2); + 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; + + test.proposeEditFeeParameters(ADMIN, ADMIN2, 50, 20, PROTOCOL_FEE_RECIPIENT, ORACLE_FEE_RECIPIENT); + + QSB::GetConfig_output config = test.getConfig(); + + 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; + + test.proposeAddRole(ADMIN, ADMIN2, (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; + + test.proposeAddRole(ADMIN, ADMIN2, (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; + + test.proposeAddRole(ADMIN, ADMIN2, (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; + + 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); + 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; + + test.proposeAddRole(ADMIN, ADMIN2, (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, 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; + + QSB::GetFilledOrders_output out = test.getFilledOrders(0, 64); + EXPECT_EQ(out.totalActive, 0u); + 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; + + // 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); + hash.set(0, (uint8)(i & 0xff)); + hash.set(1, (uint8)((i >> 8) & 0xff)); + test.getState()->forceMarkOrderFilled(hash); + } + + // 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); + QSB::IsOrderFilled_output out0 = test.isOrderFilled(hash0); + 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); + + // The last inserted hash (from round 2) is still present + QSB::OrderHash hashLast; + setMemory(hashLast, 0); + 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); +} + +// ============================================================================ +// Initialization Tests +// ============================================================================ + +TEST(ContractTestingQSB, TestInitialization) +{ + ContractTestingQSB test; + + // Check initial state + test.getState()->checkAdminCount(2); + test.getState()->checkAdminThreshold(2); + 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(USER1, 1000000); + + // 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); + + 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_RingBufferOverwritesOldestSlot) +{ + ContractTestingQSB test; + const uint64 amount = 1; + + // 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, 0, 1, i, ContractTestingQSB::createZeroAddress(), amount); + ASSERT_TRUE(out.success); + } + + // Nonce 0 exists before overwrite. + 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 overflow = test.lock(USER1, amount, 0, 1, QSB_MAX_LOCKED_ORDERS, ContractTestingQSB::createZeroAddress(), amount); + EXPECT_TRUE(overflow.success); + + // 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) +{ + 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); +} + +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); +} + +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 Multisig Tests +// ============================================================================ + +TEST(ContractTestingQSB, TestPropose_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 out = test.propose(USER1, input); + EXPECT_FALSE((bool)out.success); + EXPECT_EQ(out.reasonCode, QSBReasonNotAdmin); +} + +TEST(ContractTestingQSB, TestPropose_FailsWhenAdminExceedsConcurrentCap) +{ + ContractTestingQSB test; + + // 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"; + } + + // 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, TestPropose_AutoApprovesForProposer) +{ + ContractTestingQSB test; + + // 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; + + 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; + + 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; + + 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; + + 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); +} + +TEST(ContractTestingQSB, TestAddRole_OracleFull) +{ + ContractTestingQSB test; + + for (uint32_t i = 0; i < QSB_MAX_ORACLES; ++i) + { + id oracle(i + 1, 0, 0, 0); + 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::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; + + for (uint32_t i = 0; i < QSB_MAX_PAUSERS; ++i) + { + id pauser(i + 1, 0, 0, 0); + 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::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; + + test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Oracle, ORACLE1); + + 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); +} + +TEST(ContractTestingQSB, TestPause_ByAdmin) +{ + ContractTestingQSB test; + + QSB::Pause_output output = test.pause(ADMIN); + EXPECT_TRUE(output.success); + + test.getState()->checkPaused(true); +} + +TEST(ContractTestingQSB, TestPause_ByPauser) +{ + ContractTestingQSB test; + + 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_ByAdminProposal) +{ + ContractTestingQSB test; + + test.pause(ADMIN); + test.getState()->checkPaused(true); + + 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_FailsForNonAdmin) +{ + ContractTestingQSB test; + + test.proposeAddRole(ADMIN, ADMIN2, (uint8)QSB::Role::Pauser, PAUSER1); + test.pause(PAUSER1); + test.getState()->checkPaused(true); + + // 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); +} + +TEST(ContractTestingQSB, TestEditFeeParameters) +{ + ContractTestingQSB test; + + 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); + test.getState()->checkOracleFeeRecipient(ORACLE_FEE_RECIPIENT); +} + +TEST(ContractTestingQSB, TestEditFeeParameters_RejectsTooHighBpsFee) +{ + ContractTestingQSB test; + + QSB::Propose_input input; + setMemory(input, 0); + input.proposalType = QSBPropEditFeeParameters; + input.bpsFee = QSB_MAX_BPS_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 remain unchanged + test.getState()->checkBpsFee(0); +} + +TEST(ContractTestingQSB, TestEditFeeParameters_RejectsTooHighProtocolFee) +{ + ContractTestingQSB test; + + // 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::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); +} + +// ============================================================================ +// 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; + + 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 +} + +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; + + // 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); + + // Update oracle threshold + test.proposeEditOracleThreshold(ADMIN, ADMIN2, 67); // 2/3 + test.getState()->checkOracleThreshold(67); + + // Configure fees + test.proposeEditFeeParameters(ADMIN, ADMIN2, 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; + 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; + 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; + + // 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; + 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); +} diff --git a/test/test.vcxproj b/test/test.vcxproj index 74896bd5..4789263f 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -128,8 +128,8 @@ - - + + @@ -207,4 +207,4 @@ - \ No newline at end of file + diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index 8eebbc6f..2c57913b 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -49,6 +49,8 @@ + +