All pages

Contract model

This page specifies how contracts sit inside the transaction rules: the Deploy and Call payloads, what happens when a contract fails, how vaults delay calls, calls between contracts, receipts and the receipt root, how gas meets fees, free reads, and the state keys contracts use. It is written for protocol engineers and auditors. The sandbox, with its gas table and host calls, is on Virtual machine. Writing a contract is on Writing contracts, and the deployed contracts are on Token, AMM and escrow.

What a contract is

A contract is a WebAssembly module held in state, with an address, a native balance and a private key-value store. Each function is an export. init runs once, at deployment.

  • Only the governance account can deploy. See why deployment is governance-only.
  • Code does not change. No host call or payload replaces it, and the chain has no upgrade mechanism.
  • A contract address has no key. It is a hash of the deployer and a nonce, not of an AuthPolicy, so a transaction signed for it fails with RevealMismatch.
  • A contract reads and writes only its own storage, and sends only its own native balance. It cannot pull funds from an account.
  • Payload::Transfer to a contract credits its balance and runs no code. There is no receive hook.
  • A contract cannot deploy a contract. There is no host call for it, so factories cannot exist.
  • Host calls handle AssetId::NATIVE only. A token is a contract with books in its own storage, and the chain does not know who holds what.

Record and address

/// What is stored for a deployed contract. The code lives once under its hash.
pub struct Contract {
    pub code: Hash,
}

pub fn contract_address(deployer: &Address, nonce: u64) -> Address;
pub fn code_hash(code: &[u8]) -> Hash;

Both are BLAKE3 in derive-key mode, each with its own context:

contract_address = BLAKE3(context "qubits.sh 2026-11 contract address",
                          deployer (32 bytes) || nonce (8 bytes, little-endian))
code_hash        = BLAKE3(context "qubits.sh 2026-11 contract code", code)

nonce is the nonce of the deployment transaction, not the account's nonce after it. On a new chain the first deployment is at contract_address(governance_address(), 0).

Payloads

Transaction is unchanged by contracts and has no gas field. Two variants of Payload carry a gas limit, and Payload::gas_limit() returns 0 for all the others.

pub enum Payload {
    Transfer { to: Address, asset: AssetId, amount: Amount },  // tag 0
    SetAuth { auth: AuthPolicy },                              // tag 1
    SetVault { vault: Option<VaultPolicy> },                   // tag 2
    CancelPending { id: u64 },                                 // tag 3
    ExecutePending { account: Address, id: u64 },              // tag 4
    SetValidators { validators: Vec<Validator> },              // tag 5
    /// Puts a contract on the chain. Only the governance account can, for now.
    Deploy {                                                   // tag 6
        code: Vec<u8>,
        args: Vec<u8>,
        gas_limit: u64,
    },
    /// `contract.function(args)`, with `amount` of the native token going along.
    Call {                                                     // tag 7
        contract: Address,
        function: String,
        args: Vec<u8>,
        amount: Amount,
        gas_limit: u64,
    },
    ClaimRewards { payout: Address },                          // tag 8
}
FieldTypeLimitChecked by
Deploy.codeVec<u8>MAX_CODE_BYTES, 163,840 bytes (160 KiB)TxError::TooLarge
Deploy.args, Call.argsVec<u8>MAX_ARGS_BYTES, 16,384 bytes (16 KiB)TxError::TooLarge
Call.functionStringMAX_FUNCTION_NAME, 64 bytesTxError::TooLarge
gas_limitu64MAX_TX_GAS, 5,000,000TxError::GasTooHigh
Call.amountu128the sender's balance after the fee, less a vault's recovery reserveTxError::InsufficientBalance, TxError::RecoveryReserve
whole transactionMAX_TX_BYTES, 294,912 bytes (288 KiB)TxError::TooLarge

The chain does not read args. It hands them to the function as bytes, and each contract defines its own layout.

Encoding

Both payloads are borsh, as is everything hashed or signed. Vec<u8> and String carry a u32 little-endian length.

Deploy: 06 | u32 len | code | u32 len | args | gas_limit (u64 LE)
Call:   07 | contract (32) | u32 len | function | u32 len | args | amount (u128 LE) | gas_limit (u64 LE)

In JSON, code and args are hex and amount is a decimal string:

{"call":{"contract":"qb1w32tl3w33tcc8pn8g4xwpedcpyvrhvzs040cwqs992y7ph2nfu0smd2dv8","function":"transfer","args":"0102","amount":"5","gas_limit":7}}
{"deploy":{"code":"0061","args":"09","gas_limit":7}}

Deployment

Payload::Deploy is valid only from governance_address(), whose owner policy is always a quorum of the current validators. Any other sender gets TxError::NotGovernance. The governance account can send only SetValidators and Deploy, and anything else from it fails with NotAllowedForGovernance, so it cannot call contracts, hold a vault or transfer. It pays fees like any account, so it needs funds. See Governance.

Why deployment is governance-only

A contract runs only once a quorum of validators has signed its deployment, so in practice only contracts written by the project run. Opening deployment is a separate decision that needs a protocol change.

Three things depend on this. contracts in state stays short enough for wallets to ask each contract what it is. Open deployment would need an index by holder and protection against spam tokens. And there can be no factory, which is why the AMM is one contract holding every pool.

Open deployment: Not implemented.

Algorithm

After the common checks, contracts::deploy runs these steps, all through the undo log:

  1. Take stored = code.len() + 32 and subtract stored × STATE_BYTE from gas_limit. On underflow, finish OutOfGas with gas_used = gas_limit and write nothing.
  2. Write code/<hash>, contract/<address> and the contracts list with address appended.
  3. Run init with args, with the deployer as caller and value 0. A module with no init export deploys doing nothing.
  4. If init did not finish Done, roll back everything, including the writes of step 2.
  5. Credit the refund for unused gas to the governance account, and fill in the receipt. Receipt.contract is Some(address) only when the outcome is Done.

A failed deployment is included, pays and uses up the nonce. It leaves no code, no contract record and no entry in contracts. Two contracts with the same code share one code/<hash> entry, but each deployment pays for it in full. A rollback does not remove an entry that was already in state, because the undo log restores only what this transaction touched.

The gas of a deployment, for code of L bytes and p initial memory pages, is:

gas = (L + 32) × STATE_BYTE          state for the code and its hash
    + CALL + L × LOAD_BYTE           starting the run
    + p × PAGE                       instantiating memory
    + whatever init uses
    = 80 × L + 4,048 + 500 × p + init

MAX_TX_GAS limits deployments before MAX_CODE_BYTES does. At 80 gas a byte, the largest module that can deploy has one page, no init, and 62,443 bytes, which uses 4,999,988 gas. One more byte ended OutOfGas when run. A module of the full 163,840 bytes is a valid transaction of 167,274 bytes, is included, pays, and ends OutOfGas.

Module in out/Code bytesTransaction bytesDeployment gas
token.wasm6,67710,182681,481
escrow.wasm5,2908,724427,748
amm.wasm14,28217,7161,147,108

These were measured on a test chain with one validator key, so one signature. The token's init arguments were (owner, 1000, "Test", "TST", 6).

Calls

Validity

Staged::apply checks a transaction in this order. Failing any check gives a TxError: the transaction is invalid, stays out of the block and pays nothing.

  1. Encoded size at most MAX_TX_BYTES.
  2. genesis_hash matches, and valid_until_ms has not passed.
  3. Sizes of code, args and function.
  4. gas_limit at most MAX_TX_GAS.
  5. fee at least the required fee, and at most MAX_FEE_MULTIPLE (8) times it.
  6. The sender exists, the nonce matches, and the balance covers the fee.
  7. Policy reveal, then signatures, which go last because they are expensive.
  8. The auth policy is saved, the nonce goes up, and the fee is debited.
  9. Governance gating, then the recovery rule: a Role::Recovery transaction may only cancel or replace the owner policy, so a recovery Call fails with NotAllowedForRecovery.

Step 8 writes to a Staged overlay. If anything fails later in apply, the whole overlay is dropped, so the fee and nonce go with it.

For Payload::Call, four more checks follow:

  1. function == "init" fails with TxError::NotCallable.
  2. No contract/<address> record fails with TxError::NotAContract.
  3. A balance below amount, after the fee, fails with TxError::InsufficientBalance. For a vault with a recovery policy, one that would leave less than its recovery reserve fails with TxError::RecoveryReserve.
  4. A vault sender goes to the delay queue. Otherwise the gas is reserved and the call runs.

Execution

For an account that is not a vault:

  1. reserve_gas(gas_limit) counts the limit against the block, or fails with BlockGasFull.
  2. contracts::call creates a Run with an empty undo log, event list and call stack, and calls enter with the sender as from.
  3. The receipt goes into the overlay.
  4. gas_refund(gas_left) is credited to the sender.

Run::enter is the same code for a transaction and for a call from a contract:

  1. If stack.len() >= MAX_CALL_DEPTH or to is already on the stack, finish Reverted { reason: "call not allowed here" } with all gas left.
  2. Load the code through contract/<to>, then code/<hash>. If there is none, finish Invalid with all gas left.
  3. Mark the length of the undo log and of the event list.
  4. If value > 0, move it from from to to. If that fails, finish Reverted { reason: "insufficient balance" } with all gas left.
  5. Push to, run the VM, pop.
  6. If the outcome is not Done, roll back to the marks. The value goes back too.

The failure rule

A transaction can fail in two ways, and they have different results.

Invalid transactionFailed contract
What it isAny TxError from applyA valid transaction whose Outcome is not Done
In a blockNo. execute_block rejects a block containing oneYes
FeeNot paidPaid, less the refund for unused gas
NonceUnchangedAdvanced
Contract's workNever ran, or discardedUndone through the undo log
ResultThe node's errorReceipt with the outcome

A failing call has to pay, or anyone could burn validators' CPU for free. A node does not run code when it takes a transaction in, so a transaction that will revert is accepted, then included.

Undo log

Everything a contract changes goes through an undo log. The fee and nonce sit outside it and always stand.

enum Undo {
    Write {
        key: Vec<u8>,
        /// None if this transaction hadn't touched the key.
        previous: Option<Option<Vec<u8>>>,
    },
    Balance { address: Address, previous: Amount },
    Created(Address),
}

struct Run<'r, 'e, 'a, S: StateReader> {
    staged: &'r mut Staged<'e, 'a, S>,
    undo: Vec<Undo>,
    events: Vec<Event>,
    stack: Vec<Address>,
}

Reads go through three layers, in order: this transaction's writes, the writes of earlier transactions in the block, then state. Balances live in Staged.accounts, overlaid the same way.

Each write records what the overlay held for that key. previous has three cases. None means the transaction had not touched the key, Some(None) means it had deleted it, and Some(Some(bytes)) means it had written it. A credit to an address with no account pushes Created, then Balance.

rollback(mark, events) truncates the event list, then pops entries newest first until the log is back at mark. A Write either restores the overlay's value or removes the key from the overlay, a Balance restores the amount, and a Created removes the account. Because every enter takes its own marks, a failed callee undoes only itself and the callers below it keep what they did.

Calls between contracts

The call host call reaches Run::call, which refuses init and otherwise calls enter with the calling contract as from. value comes out of the calling contract's own balance.

No re-entrancy

A running contract cannot be called again, at any depth. enter checks the stack, which holds every running contract, so A calling B calling A fails, and so does A calling itself. Most contract thefts work through re-entry. The failed call is an ordinary failure: the outer contract gets 1 from call and can carry on. tests/contracts.rs covers this in a_contract_cannot_be_reentered.

Depth

MAX_CALL_DEPTH is 8. The stack holds the top-level contract too, so eight contracts can be running. The ninth is refused with call not allowed here, and calls_go_only_so_deep pins this.

Gas across calls

call has no gas parameter. It gives the callee all the gas left, and the caller gets back what the callee did not use.

What the callee doescall returnsCaller's gas afterwards
Finishes0What is left
Reverts1What is left
Is refused: re-entry, depth, init, value too high1Unchanged
Is not a contract1Unchanged
Has no such function1What is left after loading it
Runs out of gas10
Traps10

A failed callee is undone alone, but a trap or running out of gas also takes all the gas that was forwarded. The caller then resumes with none, so its next host call or metered block ends it OutOfGas, and the whole transaction is undone. Only a caller that does neither finishes Done. Measured with a 2,000,000 gas limit, a reverting callee left the outer call Done at 67,367 gas. A trapping callee and a looping callee each ended the outer call OutOfGas at 2,000,000 gas. A caller can only count on recovering from a revert.

A callee's revert reason does not reach its caller. Finish::early leaves output empty, and result then holds nothing. Only the outermost failure's reason goes into the receipt.

Vault calls

A vault's Payload::Call does not run at once. It goes into the delay queue like any other owner action. Without this, a stolen owner key could move what the vault holds inside contracts, past the delay. See Vaults.

pub enum DelayedAction {
    Transfer { to: Address, asset: AssetId, amount: Amount },  // tag 0
    SetAuth(AuthPolicy),                                       // tag 1
    SetVault(Option<VaultPolicy>),                             // tag 2
    /// A vault's contract call: it can move anything the vault holds in a contract, so it waits.
    Call {                                                     // tag 3
        contract: Address,
        function: String,
        args: Vec<u8>,
        amount: Amount,
        gas_limit: u64,
    },
}

Queueing

The Call passes the same validity checks, then queue adds a PendingAction. Up to MAX_PENDING_ACTIONS (16) may wait, and beyond that it fails with TooManyPending. No code runs and no gas is reserved.

The vault pays the full fee now, gas included, and the refund never comes. The receipt is Receipt::default(), so nodes keep none and GET /v1/transactions/{id} shows no receipt. amount is checked against the balance less the recovery reserve, not set aside.

Executing

After unlock_at_ms, anyone may send Payload::ExecutePending { account, id }, and the sender pays only for that transaction's own bytes, because its gas_limit() is 0. Then:

  1. The action leaves the queue.
  2. reserve_gas(gas_limit) counts the call against this block. BlockGasFull makes the ExecutePending invalid for this block, and the dropped overlay leaves the action queued.
  3. contracts::call runs with the vault as from, so the contract sees the vault as caller.
  4. The receipt becomes the receipt of the ExecutePending transaction. gas_left is dropped: nothing comes back to anyone.

A call that fails here has still left the queue and does not retry. If the vault no longer holds amount above its recovery reserve, nothing runs and the receipt says Reverted { reason: "insufficient balance" }, with 0 gas used (a_queued_call_cannot_spend_the_recovery_fee).

Until it runs, the recovery policy can remove the action with CancelPending. a_stolen_vault_key_cannot_move_tokens_through_a_contract and a_vault_call_runs_after_the_delay_as_the_vault in tests/contracts.rs cover both paths, and a_vaults_trade_waits_like_everything_else in tests/amm.rs covers a trade.

Receipts

Every applied transaction has one receipt. It records what state alone cannot say: why a call did nothing.

pub struct Receipt {
    pub outcome: Outcome,
    pub gas_used: u64,
    pub output: Vec<u8>,
    pub events: Vec<Event>,
    /// The address a deployment created.
    pub contract: Option<Address>,
}

/// How a transaction ended. Anything but `Done` undid the contract's work and still paid.
/// No text from the VM goes in here: receipts are hashed into the block.
pub enum Outcome {
    Done,                           // tag 0, the default
    Reverted { reason: Vec<u8> },   // tag 1
    OutOfGas,                       // tag 2
    Trapped,                        // tag 3
    /// The code is not a usable contract, or has no such function.
    Invalid,                        // tag 4
}

pub struct Event {
    pub contract: Address,
    pub topic: Vec<u8>,
    pub data: Vec<u8>,
}
FieldMeaning
outcomeHow the outermost run ended. The causes are on the VM page
gas_usedgas_limit - gas_left
outputWhat the outermost function gave back. Empty unless the outcome is Done
eventsEvents in the order they were emitted, across all contracts in the call. Those of a failed call are gone, so a failed transaction has none
contractThe new address, for a deployment that ended Done

Plain transactions and queued vault calls get Receipt::default(), which is 18 zero bytes in borsh. It has outcome: Done and gas_used: 0, so a Done receipt does not prove that a contract ran. A receipt with Done and 0 gas from a vault's Call means it was queued.

Receipt root

BlockPayload.receipt_root commits to the receipts of a block, in transaction order:

leaf = BLAKE3(context "qubits.sh 2026-11 receipt root leaf", borsh(receipt))
node = BLAKE3(context "qubits.sh 2026-11 receipt root node", left || right)

Pairs are hashed level by level. An unpaired last node moves up unchanged, and an empty block has Hash::ZERO. Every transaction has a leaf, plain ones included. A validator recomputes the root when it executes a block and refuses a proposal that disagrees, so gas used is consensus. A different gas count on one machine is a fork. See Virtual machine.

What is kept

A node stores only receipts that differ from Receipt::default(), in a receipts column keyed by transaction id. History expiry deletes them along with the transaction index. A node that joined from a snapshot has none for blocks below its pivot. Receipts are not in state.

JSON

A successful transfer on the token, from a test run:

{
  "outcome": "done",
  "gas_used": 190562,
  "output": "",
  "events": [
    {
      "contract": "qb1w32tl3w33tcc8pn8g4xwpedcpyvrhvzs040cwqs992y7ph2nfu0smd2dv8",
      "topic": "7472616e73666572",
      "data": "69b5f32ed0cb56494813a57cc14399d07bf6ea26acecf47bd21428488c474fbaaaad775dbfdb48cc3bf1e4740c60bff70e03a92b1676f6b289ed8d2696a459ea2c010000000000000000000000000000"
    }
  ],
  "contract": null
}

The same call for more than the balance, with reason in hex for balance too low:

{
  "outcome": { "reverted": { "reason": "62616c616e636520746f6f206c6f77" } },
  "gas_used": 130952,
  "output": "",
  "events": [],
  "contract": null
}

The other outcomes are the strings "out_of_gas", "trapped" and "invalid". topic, data, output and reason are hex.

Gas and fees

Gas is paid in the fee, so the transaction format has no gas price. One byte's worth of fee buys GAS_PER_BYTE (16) gas.

pub fn required_fee(min_fee_per_byte: u64, size: usize, gas_limit: u64) -> Amount {
    let weight = size as u128 + u128::from(gas_limit.div_ceil(GAS_PER_BYTE));
    u128::from(min_fee_per_byte) * weight
}

fn gas_refund(params: &ChainParams, gas_left: u64) -> Amount {
    Amount::from(params.min_fee_per_byte) * Amount::from(gas_left / GAS_PER_BYTE)
}

The charge rounds up and the refund rounds down, so a refund is never more than was paid for that gas. size is the encoded SignedTransaction. The fee field has a fixed width in borsh, so the size can be computed from an unsigned draft. A transaction may pay up to MAX_FEE_MULTIPLE (8) times the minimum, and only gas_left is refunded: paying above the minimum buys nothing back.

LimitValue
GAS_PER_BYTE16
MAX_TX_GAS5,000,000
MAX_BLOCK_GAS10,000,000
MAX_FEE_MULTIPLE8

Worked example

token.transfer from an account whose policy is already revealed, with a limit of 2,000,000 gas and min_fee_per_byte = 1, as a test chain ran it:

QuantityValue
Transaction size3,538 bytes
gas_limit / 16, rounded up125,000
Fee128,538
gas_used190,678
gas_left1,809,322
Refund, gas_left / 16 rounded down113,082
Net cost15,456

At min_fee_per_byte = 10000000000, each number in fee units is multiplied by that: a fee of 1,285,380,000,000,000 base units, a refund of 1,130,820,000,000,000, and a net cost of 154,560,000,000,000, which is 0.00015456 of a token.

A vault's call pays the fee and gets no refund, whatever it uses.

Block gas

reserve_gas counts declared limits, not gas used:

fn reserve_gas(&mut self, gas_limit: u64) -> Result<(), TxError> {
    let reserved = self.executor.gas_reserved + self.gas_reserved + gas_limit;
    if reserved > MAX_BLOCK_GAS {
        return Err(TxError::BlockGasFull);
    }
    self.gas_reserved += gas_limit;
    Ok(())
}

The whole limit counts, used or not, so a proposer can fill a block without running what does not fit. MAX_BLOCK_GAS is twice MAX_TX_GAS, so a block holds two calls at the highest limit. gas_limits_in_a_block_are_capped pins that.

TxError::BlockGasFull means the transaction may fit in the next block. A proposer skips it and leaves it in the mempool, where any other error drops it. A validator checking someone else's block treats it like any error and refuses the block.

Reads and checks

Admission

Executor::without_contracts() makes an executor that checks a transaction and runs nothing. Node::accept must use it, or submitting would run code for free. It goes through every rule above and skips the three places code would run: Deploy, the direct Call, and DelayedAction::Call inside ExecutePending. It reserves no gas. The receipts are Receipt::default(), and the result says nothing about what a call would do. A vault's call still queues, because that runs no code.

View and simulate

pub struct Simulated {
    pub receipt: Receipt,
    /// The caller's native balance after the call, before any fee.
    pub balance_after: Amount,
}

pub fn simulate(
    state: &impl StateReader,
    env: BlockEnv,
    caller: &Address,
    contract: &Address,
    function: &str,
    args: &[u8],
    amount: Amount,
) -> Result<Simulated, TxError>;

pub fn view(
    state: &impl StateReader,
    env: BlockEnv,
    contract: &Address,
    function: &str,
    args: &[u8],
) -> Result<Receipt, TxError>;

simulate runs a call against this state as caller, with amount going along, for free. view is simulate with the all-zero address as caller and no amount.

Both give the run MAX_TX_GAS. init fails with NotCallable, and an unknown address with NotAContract. Writes land in an overlay that is then dropped, so nothing is kept. There is no signature, fee, nonce, gas reservation or vault queue: a simulated call from a vault runs at once. Too large an amount gives a receipt with Reverted { reason: "insufficient balance" }, not an error.

State is public, so acting as anyone reveals nothing. This is how the wallet shows what a call will do without taking a website's word for it. See Wallet.

Node::simulate runs against the latest committed state, with height one above the head and timestamp_ms the larger of the clock and the head's time plus one. A real block can have a different height and time, so a call that reads them can behave differently.

Over HTTP this is POST /v1/contracts/{address}/view, which shares the submission permits because it runs code for free. The request is ViewRequest, and the response is the receipt fields flattened, plus balance_after. See API.

pub struct ViewRequest {
    pub function: String,
    pub args: Vec<u8>,            // hex in JSON
    pub caller: Option<Address>,  // nobody by default
    pub amount: Amount,           // decimal string in JSON, 0 by default
}

State keys

All keys are in the state tree, so all of this goes into state_root and comes with a snapshot. Events and receipts do not.

KeyValue
account/ + address (32)borsh Account. A contract's native balance is here, created at its first credit
contract/ + address (32)borsh Contract: the 32-byte code hash
code/ + code hash (32)The module's bytes
storage/ + address (32) + nameThe value's bytes, as the contract wrote them
contractsborsh Vec<Address>: every contract in deployment order
validatorsborsh ValidatorSet

storage/ keys start with an address of fixed length, so one contract cannot reach another's keys. name is at most MAX_KEY_BYTES (128) and a value at most MAX_VALUE_BYTES (16 KiB).

read_contracts(state) returns the list. It is state and not an index beside it, so a node that joined from a snapshot has it too, and it serves GET /v1/contracts. A failed deployment is not in it. Every deployment rewrites the whole list, which works because only governance deploys. If that ever opens up, wallets will need something better than asking each contract.

Transaction errors

These are the TxError values specific to contracts. All of them make the transaction invalid.

ErrorMessageWhen
NotGovernanceonly the governance account can change validators or deploy contractsDeploy from any other account
NotAllowedForGovernancethe governance account can only change the validator set and deploy contractsA Call or anything else from governance
NotAContractno contract at that addressCall to an address with no record
NotCallableinit runs once, at deploymentCall of init
GasTooHighgas limit is above 5000000gas_limit above MAX_TX_GAS
BlockGasFullno gas left in this blockThe limit does not fit in the block
NotAllowedForRecoveryrecovery may only cancel pending actions or replace the owner policyA Call signed by recovery
TooManyPendingvault already has 16 pending actionsA vault's call with a full queue
RecoveryReservea vault with a recovery policy keeps N for its recovery feeA vault's call that would spend into its recovery reserve
Storagestorage errorThe node itself failed, which stops the block

Fatal from the VM becomes TxError::Storage. It means a database error, not anything a contract did.

Source

  • chain/crates/qubits-execution/src/lib.rs: Staged::apply, reserve_gas, gas_refund, simulate, view, read_contracts, keys
  • chain/crates/qubits-execution/src/contracts.rs: Run, Undo, enter, call, deploy
  • chain/crates/qubits-types/src/contract.rs: Contract, Event, Outcome, Receipt, contract_address, code_hash, receipt_root
  • chain/crates/qubits-types/src/transaction.rs: Payload
  • chain/crates/qubits-types/src/account.rs: DelayedAction, PendingAction
  • chain/crates/qubits-types/src/client.rs: required_fee
  • chain/crates/qubits-types/src/lib.rs: limits
  • chain/crates/qubits-types/src/rpc.rs: ViewRequest, ViewResponse
  • chain/crates/qubits-node/src/app.rs: proposing around BlockGasFull, the receipt root check, which receipts are kept
  • chain/crates/qubits-node/src/lib.rs: Node::accept, Node::simulate
  • chain/crates/qubits-execution/tests/contracts.rs, chain/crates/qubits-execution/tests/amm.rs