Understanding gas computation, fee structure, and storage costs in IOTA transactions.
Gas is the unit used to measure the computational and storage resources consumed by transactions on IOTA. Every transaction must pay gas fees to compensate validators and prevent network abuse.
pub struct GasData { // Coins used to pay for gas payment: Vec<ObjectRef>, // Address receiving gas rebate owner: IotaAddress, // Maximum gas units to spend budget: u64, // Price per gas unit price: u64,}
The gas budget is the maximum amount (in MIST, the smallest IOTA unit) the transaction can spend:
// Set gas budgetlet gas_budget = 10_000_000; // 0.01 IOTA (10 million MIST)// Transaction will abort if it exceeds budgetif gas_used > gas_budget { return Err(ExecutionError::InsufficientGas);}
If a transaction runs out of gas during execution, it aborts and all effects are reverted. However, gas used up to that point is still charged.
// Gas price in MIST per gas unitlet gas_price = 1000; // Must be >= reference gas price// Total computation costlet total_cost = gas_units_used * gas_price;
Gas price requirements:
Must be ≥ reference gas price (RGP) set by validators
Must be ≤ maximum gas price (protocol config)
Higher prices don’t speed up execution but affect validator rewards
The reference gas price (RGP) is determined by validators through a coordination mechanism. It typically remains stable unless network conditions change significantly.
// Examples of instruction costs (in gas units)const LOAD_COST: u64 = 1;const STORE_COST: u64 = 1;const BRANCH_COST: u64 = 1;const CALL_COST: u64 = 10;const MUL_COST: u64 = 3;
The Move VM meters execution at the bytecode level:
public fun expensive_function(n: u64): u64 { let mut sum = 0; let mut i = 0; // Loop costs gas for each iteration while (i < n) { sum = sum + i; // arithmetic costs gas i = i + 1; // increment costs gas }; // loop check costs gas sum}// Total gas depends on n
public fun create_object(ctx: &mut TxContext) { let obj = MyObject { id: object::new(ctx), data: vector::empty(), }; transfer::transfer(obj, ctx.sender()); // Storage cost charged for new object}
Mutating existing objects (if size increases)
public fun add_data(obj: &mut MyObject, data: vector<u8>) { vector::append(&mut obj.data, data); // Additional storage cost if object grows}
Only the increase in storage size is charged when mutating objects. If an object shrinks, storage rebate is provided.
When objects are deleted or shrunk, storage rebate is returned:
// Deleting an objectpublic fun delete_object(obj: MyObject) { let MyObject { id, data } = obj; object::delete(id); // Storage rebate credited to transaction sender}
pub enum TransactionKind { // User transaction - pays gas ProgrammableTransaction(ProgrammableTransaction), // System transactions - no gas ChangeEpoch(ChangeEpoch), ConsensusCommitPrologue(ConsensusCommitPrologue), // ...}
System transactions:
Are generated by validators
Execute protocol-level operations
Use unmetered gas status
Only the system can create system transactions. User transactions always pay gas.
// Bad: multiple transactionstransaction1: process_item(item1);transaction2: process_item(item2);transaction3: process_item(item3);// Good: single programmable transactionpublic fun process_batch(items: vector<Item>) { let mut i = 0; while (i < vector::length(&items)) { process_item(*vector::borrow(&items, i)); i = i + 1; }}
// Expensive: nested loopspublic fun find_pairs(items: &vector<Item>): vector<(Item, Item)> { let mut pairs = vector::empty(); let mut i = 0; while (i < vector::length(items)) { let mut j = i + 1; while (j < vector::length(items)) { // O(n²) complexity vector::push_back(&mut pairs, (*vector::borrow(items, i), *vector::borrow(items, j))); j = j + 1; }; i = i + 1; }; pairs}// Better: use efficient data structurespublic fun find_pairs_optimized(items: &vector<Item>): vector<(Item, Item)> { // Use indexing or other O(n) approach}