Skip to content

Commit 7bb7343

Browse files
Merge pull request #699 from qubic/develop (Release v1.272.0)
Release v1.272.0
2 parents cee62c5 + fa6c662 commit 7bb7343

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+4623
-2741
lines changed

doc/execution_fees.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Contract Execution Fees
2+
3+
## Overview
4+
5+
Every smart contract in Qubic has an **execution fee reserve** that determines whether the contract can execute its procedures. This reserve is stored in Contract 0's state and is initially funded during the contract's IPO (Initial Public Offering). Contracts must maintain a positive execution fee reserve to remain operational. It is important to note that these execution fees are different from the fees a user pays to a contract upon calling a procedure. To avoid confusion we will call the fees a user pays to a contract 'invocation reward' throughout this document.
6+
7+
8+
9+
## Fee Management
10+
11+
Each contract's execution fee reserve is stored in Contract 0's state in an array `contractFeeReserves[MAX_NUMBER_OF_CONTRACTS]`. The current value of the executionFeeReserve can be queried with the function `qpi.queryFeeReserve(contractIndex)` and returns a `sint64`.
12+
13+
When a contract's IPO completes, the execution fee reserve is initialized based on the IPO's final price. If the `finalPrice > 0`, the reserve is set to `finalPrice * NUMBER_OF_COMPUTORS` (676 computors). However, if the IPO fails and `finalPrice = 0`, the contract is marked as failed with `ContractErrorIPOFailed` and the reserve remains 0 and can't be filled anymore. A contract which failed the IPO will remain unusable.
14+
15+
Contracts can refill their execution fee reserves in the following ways:
16+
17+
- **Contract internal burning**: Any contract procedure can burn its own QU using `qpi.burn(amount)` to refill its own reserve, or `qpi.burn(amount, targetContractIndex)` to refill another contract's reserve.
18+
- **External refill via QUtil**: Anyone can refill any contract's reserve by sending QU to the QUtil contract's `BurnQubicForContract` procedure with the target contract index. All sent QU is burned and added to the target contract's reserve.
19+
- **Legacy QUtil burn**: QUtil provides a `BurnQubic` procedure that burns to QUtil's own reserve specifically.
20+
21+
The execution fee system follows a key principle: **"The Contract Initiating Execution Pays"**. When a user initiates a transaction, the user's destination contract must have a positive executionFeeReserve. When a contract initiates an operation (including any callbacks it triggers), that contract must have positive executionFeeReserve.
22+
23+
Currently, execution fees are checked (contracts must have `executionFeeReserve > 0`) but **not yet deducted** based on actual computation. Future implementation will measure execution time and resources per procedure call, deduct proportional fees from the reserve.
24+
25+
## What Operations Require Execution Fees
26+
27+
The execution fee system checks whether a contract has positive `executionFeeReserve` at different entry points. The table below summarizes when fees are checked and who pays:
28+
29+
| Entry Point | Initiator | executionFeeReserve Checked | Code Location |
30+
|------------|-----------|----------------------------|---------------|
31+
| System procedures (`BEGIN_TICK`, `END_TICK`, etc.) | System | ✅ Contract must have positive reserve | qubic.cpp |
32+
| User procedure call | User | ✅ Contract must have positive reserve | qubic.cpp |
33+
| Contract-to-contract procedure | Contract A | ✅ Called contract (B) must have positive reserve, otherwise error is returned to caller | contract_exec.h |
34+
| Contract-to-contract function | Contract A | ✅ Called contract (B) must have positive reserve, otherwise error is returned to caller | contract_exec.h |
35+
| Contract-to-contract callback (`POST_INCOMING_TRANSFER`, etc.) | System | ❌ Not checked (callbacks execute regardless of reserve) | contract_exec.h |
36+
| Epoch transistion system procedures (`BEGIN_EPOCH`, `END_EPOCH`) | System | ❌ Not checked | qubic.cpp |
37+
| Revenue donation (`POST_INCOMING_TRANSFER`) | System | ❌ Not checked | qubic.cpp |
38+
| IPO refund (`POST_INCOMING_TRANSFER`) | System | ❌ Not checked | ipo.h |
39+
| User functions | User | ❌ Never checked (read-only) | N/A |
40+
41+
**Basic system procedures** (`BEGIN_TICK`, `END_TICK`) require the contract to have `executionFeeReserve > 0`. If the reserve is depleted, these procedures are skipped and the contract becomes dormant. These procedures are invoked by the system directly.
42+
43+
**Epoch transistion system procedures** `BEGIN_EPOCH`, `END_EPOCH` are executed even with a non-positive `executionFeeReserve` to keep contract state in a valid state.
44+
45+
**User procedure calls** check the contract's execution fee reserve before execution. If `executionFeeReserve <= 0`, the transaction fails and any attached amount is refunded to the user. If the contract has fees, the procedure executes normally and may trigger `POST_INCOMING_TRANSFER` callback first if amount > 0.
46+
47+
**User functions** (read-only queries) are always available regardless of executionFeeReserve. They are defined with `PUBLIC_FUNCTION()` or `PRIVATE_FUNCTION()` macros, provide read-only access to contract state, and cannot modify state or trigger procedures.
48+
49+
**Contract-to-contract procedure calls** via `INVOKE_OTHER_CONTRACT_PROCEDURE` check that the **called contract (B) has positive executionFeeReserve**. If Contract B has insufficient fees (`executionFeeReserve <= 0`), the call fails and returns `CallErrorInsufficientFees` to Contract A. The procedure is not executed, and Contract A can check the error via the `interContractCallError` variable (or a custom error variable when using `INVOKE_OTHER_CONTRACT_PROCEDURE_E`). Contract developers should check the error after invoking procedures or proactively verify the called contract's fee reserve with `qpi.queryFeeReserve(contractIndex)` before invoking it.
50+
51+
**Contract-to-contract function calls** via `CALL_OTHER_CONTRACT_FUNCTION` also check that the **called contract (B) has positive executionFeeReserve**. If Contract B has insufficient fees or is in an error state, the call fails and returns `CallErrorInsufficientFees` or `CallErrorContractInErrorState` to Contract A. The function is not executed, and Contract A can check the error via the `interContractCallError` variable (or a custom error variable when using `CALL_OTHER_CONTRACT_FUNCTION_E`). This graceful error handling allows Contract A to continue execution and handle failures appropriately.
52+
53+
**Contract-to-contract callbacks** (`POST_INCOMING_TRANSFER`, `PRE_ACQUIRE_SHARES`, `POST_ACQUIRE_SHARES`, etc.) are system-initiated and **do not check executionFeeReserve**. These callbacks execute regardless of the called contract's fee reserve status, allowing contracts to receive system-initiated transfers and notifications even when dormant. This design ensures that contracts can receive revenue donations, IPO refunds, and other system transfers without requiring positive fee reserves.
54+
55+
Example: Contract A (executionFeeReserve = 1000) transfers 500 QU to Contract B (executionFeeReserve = 0) using `qpi.transfer()`. The transfer succeeds and Contract B's `POST_INCOMING_TRANSFER` callback executes regardless of Contract B having no fees, because the callback is system-initiated. However, if Contract A tries to invoke a procedure of Contract B using `INVOKE_OTHER_CONTRACT_PROCEDURE`, the call will fail and return `CallErrorInsufficientFees`. Contract A should check `interContractCallError` after the call and handle the error gracefully (e.g., skip the operation or use fallback logic).
56+
57+
**System-initiated transfers** (revenue donations and IPO refunds) do not require the recipient contract to have positive executionFeeReserve. The `POST_INCOMING_TRANSFER` callback executes regardless of the destination's reserve status. These are system-initiated transfers that contracts didn't request, so contracts should be able to receive system funds even if dormant.
58+
59+
## Best Practices
60+
61+
### For Contract Developers
62+
63+
1. **Plan for sustainability**: Charge invocation rewards for running user procedures
64+
2. **Burn collected invocation rewards**: Regularly call `qpi.burn()` to replenish executionFeeReserve
65+
3. **Monitor reserve**: Implement a function to expose current reserve level
66+
4. **Graceful degradation**: Consider what happens when reserve runs low
67+
5. **Handle inter-contract call errors**: After using `INVOKE_OTHER_CONTRACT_PROCEDURE`, check the `interContractCallError` variable to verify the call succeeded. Handle errors gracefully (e.g., skip operations, use fallback logic). You can also proactively verify the called contract has positive `executionFeeReserve` using `qpi.queryFeeReserve(contractIndex) > 0` before calling.
68+
69+
### For Contract Users
70+
71+
1. **Check contract status**: Before using a contract, verify it has positive executionFeeReserve
72+
2. **Transaction failures**: If your transaction fails due to insufficient execution fees reserve, the attached amount will be automatically refunded
73+
3. **No funds lost**: The system ensures amounts are refunded if a contract cannot execute

lib/platform_common/platform_common.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<ClInclude Include="processor.h" />
2121
<ClInclude Include="compiler_warnings.h" />
2222
<ClInclude Include="sleep.h" />
23+
<ClInclude Include="sorting.h" />
2324
</ItemGroup>
2425
<ItemGroup>
2526
<ClCompile Include="edk2_mdepkg\Library\BaseLib\LongJump.c" />

lib/platform_common/platform_common.vcxproj.filters

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
</ClInclude>
2020
<ClInclude Include="sleep.h" />
2121
<ClInclude Include="processor.h" />
22+
<ClInclude Include="compiler_warnings.h" />
23+
<ClInclude Include="sorting.h" />
2224
</ItemGroup>
2325
<ItemGroup>
2426
<ClCompile Include="edk2_mdepkg\Library\BaseLib\LongJump.c">

lib/platform_common/sorting.h

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#pragma once
2+
3+
enum class SortingOrder
4+
{
5+
SortAscending,
6+
SortDescending,
7+
};
8+
9+
// Lomuto's partition scheme for quick sort:
10+
// Uses the last element in the range as pivot. Swaps elements until all elements that should go before the pivot
11+
// (according to the sorting order) are on the left side of the pivot and all others are on the right side of the pivot.
12+
// Returns the index of the pivot in the range after partitioning.
13+
template <typename T>
14+
unsigned int partition(T* range, int first, int last, SortingOrder order)
15+
{
16+
constexpr auto swap = [](T& a, T& b) { T tmp = b; b = a; a = tmp; };
17+
18+
T pivot = range[last];
19+
20+
// Next available index to swap to. Elements with indices < nextIndex are certain to go before the pivot.
21+
int nextIndex = first;
22+
for (int i = first; i < last; ++i)
23+
{
24+
bool shouldGoBefore = range[i] < pivot; // SortAscending
25+
if (order == SortingOrder::SortDescending)
26+
shouldGoBefore = !shouldGoBefore;
27+
28+
if (shouldGoBefore)
29+
{
30+
swap(range[nextIndex], range[i]);
31+
++nextIndex;
32+
}
33+
}
34+
35+
// move pivot after all elements that should go before the pivot
36+
swap(range[nextIndex], range[last]);
37+
38+
return nextIndex;
39+
}
40+
41+
// Sorts the elements from range[first] to range[last] according to the given `order`.
42+
// The sorting happens in place and requires type T to have the comparison operator < defined.
43+
template <typename T>
44+
void quickSort(T* range, int first, int last, SortingOrder order)
45+
{
46+
if (first >= last)
47+
return;
48+
49+
// pivot is the partitioning index, range[pivot] is in correct position
50+
unsigned int pivot = partition(range, first, last, order);
51+
52+
// recursively sort smaller ranges to the left and right of the pivot
53+
quickSort(range, first, pivot - 1, order);
54+
quickSort(range, pivot + 1, last, order);
55+
56+
return;
57+
}

src/Qubic.vcxproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
<ClInclude Include="assets\net_msg_impl.h" />
2424
<ClInclude Include="common_buffers.h" />
2525
<ClInclude Include="contracts\ComputorControlledFund.h" />
26+
<ClInclude Include="contracts\ComputorControlledFund_old.h" />
2627
<ClInclude Include="contracts\Qdraw.h" />
2728
<ClInclude Include="contracts\Qswap.h" />
28-
<ClInclude Include="contracts\Qswap_old.h" />
2929
<ClInclude Include="contracts\RandomLottery.h" />
3030
<ClInclude Include="contracts\SupplyWatcher.h" />
3131
<ClInclude Include="contracts\EmptyTemplate.h" />
@@ -52,6 +52,7 @@
5252
<ClInclude Include="contract_core\contract_action_tracker.h" />
5353
<ClInclude Include="contract_core\contract_def.h" />
5454
<ClInclude Include="contract_core\contract_exec.h" />
55+
<ClInclude Include="contract_core\execution_time_accumulator.h" />
5556
<ClInclude Include="contract_core\ipo.h" />
5657
<ClInclude Include="contract_core\pre_qpi_def.h" />
5758
<ClInclude Include="contract_core\qpi_asset_impl.h" />
@@ -81,6 +82,7 @@
8182
<ClInclude Include="network_messages\contract.h" />
8283
<ClInclude Include="network_messages\custom_mining.h" />
8384
<ClInclude Include="network_messages\entity.h" />
85+
<ClInclude Include="network_messages\execution_fees.h" />
8486
<ClInclude Include="network_messages\header.h" />
8587
<ClInclude Include="network_messages\logging.h" />
8688
<ClInclude Include="network_messages\public_peers.h" />
@@ -102,6 +104,7 @@
102104
<ClInclude Include="platform\console_logging.h" />
103105
<ClInclude Include="platform\common_types.h" />
104106
<ClInclude Include="platform\profiling.h" />
107+
<ClInclude Include="platform\quorum_value.h" />
105108
<ClInclude Include="platform\random.h" />
106109
<ClInclude Include="platform\read_write_lock.h" />
107110
<ClInclude Include="platform\stack_size_tracker.h" />
@@ -125,6 +128,7 @@
125128
<ClInclude Include="ticking\ticking.h" />
126129
<ClInclude Include="ticking\tick_storage.h" />
127130
<ClInclude Include="ticking\pending_txs_pool.h" />
131+
<ClInclude Include="ticking\execution_fee_report_collector.h" />
128132
<ClInclude Include="vote_counter.h" />
129133
</ItemGroup>
130134
<ItemGroup>

src/Qubic.vcxproj.filters

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,17 +274,26 @@
274274
<ClInclude Include="network_messages\custom_mining.h">
275275
<Filter>network_messages</Filter>
276276
</ClInclude>
277+
<ClInclude Include="network_messages\execution_fees.h">
278+
<Filter>network_messages</Filter>
279+
</ClInclude>
277280
<ClInclude Include="platform\virtual_memory.h" />
278281
<ClInclude Include="platform\profiling.h">
279282
<Filter>platform</Filter>
280283
</ClInclude>
284+
<ClInclude Include="platform\quorum_value.h">
285+
<Filter>platform</Filter>
286+
</ClInclude>
281287
<ClInclude Include="revenue.h" />
282288
<ClInclude Include="contract_core\qpi_mining_impl.h">
283289
<Filter>contract_core</Filter>
284290
</ClInclude>
285291
<ClInclude Include="ticking\pending_txs_pool.h">
286292
<Filter>ticking</Filter>
287293
</ClInclude>
294+
<ClInclude Include="ticking\execution_fee_report_collector.h">
295+
<Filter>ticking</Filter>
296+
</ClInclude>
288297
<ClInclude Include="contracts\Qdraw.h">
289298
<Filter>contracts</Filter>
290299
</ClInclude>
@@ -297,9 +306,12 @@
297306
<ClInclude Include="network_messages\network_message_type.h">
298307
<Filter>network_messages</Filter>
299308
</ClInclude>
300-
<ClInclude Include="contracts\Qswap_old.h">
309+
<ClInclude Include="contracts\ComputorControlledFund_old.h">
301310
<Filter>contracts</Filter>
302311
</ClInclude>
312+
<ClInclude Include="contract_core\execution_time_accumulator.h">
313+
<Filter>contract_core</Filter>
314+
</ClInclude>
303315
</ItemGroup>
304316
<ItemGroup>
305317
<Filter Include="platform">

src/contract_core/contract_def.h

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,11 @@
8989
#define CONTRACT_INDEX CCF_CONTRACT_INDEX
9090
#define CONTRACT_STATE_TYPE CCF
9191
#define CONTRACT_STATE2_TYPE CCF2
92+
#ifdef OLD_CCF
93+
#include "contracts/ComputorControlledFund_old.h"
94+
#else
9295
#include "contracts/ComputorControlledFund.h"
96+
#endif
9397

9498
#undef CONTRACT_INDEX
9599
#undef CONTRACT_STATE_TYPE
@@ -139,11 +143,7 @@
139143
#define CONTRACT_INDEX QSWAP_CONTRACT_INDEX
140144
#define CONTRACT_STATE_TYPE QSWAP
141145
#define CONTRACT_STATE2_TYPE QSWAP2
142-
#ifdef OLD_QSWAP
143-
#include "contracts/Qswap_old.h"
144-
#else
145146
#include "contracts/Qswap.h"
146-
#endif
147147

148148
#undef CONTRACT_INDEX
149149
#undef CONTRACT_STATE_TYPE
@@ -195,8 +195,6 @@
195195
#define CONTRACT_STATE2_TYPE QIP2
196196
#include "contracts/QIP.h"
197197

198-
#ifndef NO_QRAFFLE
199-
200198
#undef CONTRACT_INDEX
201199
#undef CONTRACT_STATE_TYPE
202200
#undef CONTRACT_STATE2_TYPE
@@ -207,11 +205,12 @@
207205
#define CONTRACT_STATE2_TYPE QRAFFLE2
208206
#include "contracts/QRaffle.h"
209207

210-
#endif
211-
212208
// new contracts should be added above this line
213209

214210
#ifdef INCLUDE_CONTRACT_TEST_EXAMPLES
211+
// forward declaration, defined in qpi_spectrum_impl.h
212+
static void setContractFeeReserve(unsigned int contractIndex, long long newValue);
213+
215214
constexpr unsigned short TESTEXA_CONTRACT_INDEX = (CONTRACT_INDEX + 1);
216215
#undef CONTRACT_INDEX
217216
#undef CONTRACT_STATE_TYPE
@@ -312,9 +311,7 @@ constexpr struct ContractDescription
312311
{"RL", 182, 10000, sizeof(RL)}, // proposal in epoch 180, IPO in 181, construction and first use in 182
313312
{"QBOND", 182, 10000, sizeof(QBOND)}, // proposal in epoch 180, IPO in 181, construction and first use in 182
314313
{"QIP", 189, 10000, sizeof(QIP)}, // proposal in epoch 187, IPO in 188, construction and first use in 189
315-
#ifndef NO_QRAFFLE
316314
{"QRAFFLE", 192, 10000, sizeof(QRAFFLE)}, // proposal in epoch 190, IPO in 191, construction and first use in 192
317-
#endif
318315
// new contracts should be added above this line
319316
#ifdef INCLUDE_CONTRACT_TEST_EXAMPLES
320317
{"TESTEXA", 138, 10000, sizeof(TESTEXA)},
@@ -429,15 +426,19 @@ static void initializeContracts()
429426
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(RL);
430427
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QBOND);
431428
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QIP);
432-
#ifndef NO_QRAFFLE
433429
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QRAFFLE);
434-
#endif
435430
// new contracts should be added above this line
436431
#ifdef INCLUDE_CONTRACT_TEST_EXAMPLES
437432
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA);
438433
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXB);
439434
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXC);
440435
REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXD);
436+
437+
// fill execution fee reserves for test contracts
438+
setContractFeeReserve(TESTEXA_CONTRACT_INDEX, 10000);
439+
setContractFeeReserve(TESTEXB_CONTRACT_INDEX, 10000);
440+
setContractFeeReserve(TESTEXC_CONTRACT_INDEX, 10000);
441+
setContractFeeReserve(TESTEXD_CONTRACT_INDEX, 10000);
441442
#endif
442443
}
443444

0 commit comments

Comments
 (0)