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 @@
+
+