Skip to main content

Cycles and Resources

Cycles are the computational currency of the Internet Computer. Every operation your canister performs costs cycles, and cycle exhaustion is a runtime resource concern outside Sector9's proof model.

What Are Cycles

Cycles measure computational resources on the IC:

ResourceCycles Cost
CPU instructionsPer instruction
Memory usagePer byte stored
Inter-canister callsPer message
HTTP outcallsPer request
Cryptographic operationsPer signature

Your canister maintains a cycle balance. As it executes, the IC deducts cycles. If the balance is insufficient for execution or storage, runtime calls can fail and canister operation can be interrupted by the IC.

The Cycles API

Sector9 exposes the current cycle helpers through mo:core/Cycles. The module wraps IC cycle primitives but does not give the verifier a model of cycle accounting.

Core Functions

import Cycles "mo:core/Cycles";

// Query your canister's current cycle balance
let myBalance : Nat = Cycles.balance();

// Check cycles received in the current call
let received : Nat = Cycles.available();

// Accept cycles from the caller
let accepted : Nat = Cycles.accept<system>(10_000_000);

// Check cycles refunded from the last await
let refund : Nat = Cycles.refunded();

// Burn cycles intentionally
let burned : Nat = Cycles.burn<system>(1_000_000);

Function Reference

FunctionSignaturePurpose
balance()() -> NatCurrent canister cycle balance
available()() -> NatCycles received in this call (minus accepted)
accept<system>(n)Nat -> NatTransfer cycles from available to balance
refunded()() -> NatCycles returned from last await
burn<system>(n)Nat -> NatBurn cycles intentionally

The <system> capability marker indicates these functions interact with IC system state.

Cycle Transfer Semantics

Cycles flow between canisters through a push model:

Sending Cycles

try { await (with cycles = 15_000_000) targetCanister.operation(); } catch (_) {};

The parenthetical syntax (with cycles = amount) attaches cycles to a specific call. Legacy primitive add/call patterns exist in low-level Motoko tests, but new Sector9 code should use the parenthetical syntax.

Receiving Cycles

public func acceptDonation() : async Nat {
let offered = Cycles.available();
let accepted = Cycles.accept<system>(offered);
accepted
}

Refund Mechanism

Unaccepted cycles automatically return to the caller:

public func sendWithRefund() : async Nat {
try { await (with cycles = 10_000_000) targetCanister.operation(); } catch (_) {}; // accepts 3_000_000
Cycles.refunded() // returns 7_000_000
}

If the callee rejects or traps before accepting cycles, the attached cycles are refunded. Cycles already accepted by the callee follow IC runtime rules and should not be treated as proof-visible state.

Cycle Flow Diagram

Caller                           Callee
------ ------
balance = 100M balance = 50M

├── await (with cycles = 20M) operation()
│ ──────► available() = 20M
│ balance now 80M │
│ ├── accept<system>(15M)
│ │ balance now 65M
│ │
│ ◄────────────────────────── return

├── refunded() = 5M
│ balance now 85M

Cost Estimation

The IC provides cost estimation functions for planning:

import Prim "mo:⛔";

// Estimate cost to create a new canister
let createCost : Nat = Prim.costCreateCanister();

// Estimate cost for an inter-canister call
let callCost : Nat = Prim.costCall(methodNameLength, payloadSize);

// Estimate cost for HTTP outcall
let httpCost : Nat = Prim.costHttpRequest(requestSize, maxResponseBytes);

These estimates help ensure you attach sufficient cycles before operations. Sector9 does not prove the estimates are sufficient.

Cycle Limits

Cycle amounts use 128-bit unsigned integers. The maximum is 2^128 - 1:

// This traps or rejects: exceeds maximum
await (with cycles = 0x1_00000000_00000000_00000000_00000000) target.operation();
// Error: "cannot add more than 2^128 cycles"

Attempting to add more than the maximum causes an immediate trap.

Verification and Cycles

Sector9 does not model cycle accounting. Cycles are explicitly outside the verification boundary.

What Sector9 Verifies

What Sector9 Does Not Verify

  • Cycle balance sufficiency
  • Cycle exhaustion
  • Cost estimation accuracy
  • Refund handling correctness

Why Cycles Are Out of Scope

The verification model focuses on algebraic reasoning about values, state, and control flow. Cycle accounting depends on:

  • Runtime execution costs (unpredictable)
  • IC infrastructure pricing (external)
  • Canister memory usage (dynamic)
  • Network conditions (variable)

These factors are outside what SMT-based verification can reason about.

Cycles Operations in Verification

When the verifier encounters core cycle operations, it treats them as trusted runtime calls with opaque return values:

public func deposit() : async ()
modifies savings
requires true; // No cycle-related preconditions
{
let amount = Cycles.available(); // Verifier: opaque Nat
let accepted = Cycles.accept<system>(amount); // Verifier: opaque Nat
savings += accepted; // Verifier: normal arithmetic
}

The verifier does not prove relationships such as "accepted cycles are less than available cycles" or "balance increased by the accepted amount" unless you model those relationships explicitly in your own state and contracts.

Best Practices

1. Check Balance Before Expensive Operations

public func performExpensive() : async () {
if (Cycles.balance() < minimumRequired) {
trap true "Insufficient cycles";
};
// Proceed with operation
}

2. Accept Cycles Explicitly

Always accept the cycles you need, not more:

public func acceptPayment(required : Nat) : async Bool {
let available = Cycles.available();
if (available < required) {
return false; // Reject: insufficient payment
};
let _ = Cycles.accept<system>(required); // Accept exactly what's needed
true // Excess refunds automatically
}

3. Use Parenthetical Syntax

Prefer the new syntax for clarity:

// Clear: cycles attached to specific call
try { await (with cycles = 10_000_000) target.method(); } catch (_) {};

4. Handle Refunds

Check refunds after calls to understand actual costs:

public func transferWithCost() : async Nat {
let sent = 10_000_000;
try { await (with cycles = sent) target.operation(); } catch (_) {};
let refund = Cycles.refunded();
sent - refund // Actual cost
}

Common Pitfalls

Forgetting to Accept

Cycles available in a call are not automatically credited:

// Wrong: cycles are lost (refunded to caller)
public func badDeposit() : async () {
// Cycles.available() > 0 but we never accept
}

// Correct: explicitly accept cycles
public func goodDeposit() : async () {
let _ = Cycles.accept<system>(Cycles.available());
}

Overflow on Attached Cycles

Cycle amounts are bounded:

await (with cycles = largeAmount) target.method();

Summary

  • Cycles are the IC's computational currency that powers canister execution
  • The mo:core/Cycles module provides balance, accept, burn, and refund operations
  • Cycles transfer uses a push model: sender attaches cycles, receiver accepts them
  • Unaccepted cycles automatically refund to the caller
  • Sector9 does not verify cycle sufficiency - it is a runtime concern
  • Use the parenthetical syntax (with cycles = n) for clarity
  • Always explicitly accept cycles you need

See Also