Cycles
Usage of a canister's resources on ICP is measured and paid for in cycles.
In Motoko programs deployed on ICP, each actor represents a canister and has an associated balance of cycles. The ownership of cycles can be transferred between actors. Cycles are selectively sent and received through shared function calls. A caller can choose to transfer cycles with a call, and a callee can choose to accept cycles that are made available by the caller. Unless explicitly instructed, no cycles are transferred by callers or accepted by callees.
Callees can accept all, some, or none of the available cycles up to limit determined by their actor’s current balance. Any remaining cycles are refunded to the caller. If a call traps, all its accompanying cycles are automatically refunded to the caller without loss.
Motoko is adopting dedicated syntax and types to support safer programming with cycles. Users can now attach (where cycles = <amount>) as a prefix to message sends and async expressions.
This new syntax will eventually obsolete the use of ExperimentalCycles.add<system>(cycles) in the examples that follow.
For now (and until officially deprecating it), we provide a temporary way to manage cycles through a low-level imperative API provided by the ExperimentalCycles library in package base.
This library is subject to change and likely to be replaced by more high-level support for cycles in later versions of Motoko. See Async data for further usage information about parentheticals (such as attaching cycles) on message sends.
The ExperimentalCycles Library
The ExperimentalCycles library provides imperative operations for observing an actor’s current balance of cycles, transferring cycles and observing refunds.
The library provides the following operations:
- func balance() : (amount : Nat): Returns the actor’s current balance of cycles as- amount. Function- balance()is stateful and may return different values after calls to- accept(n), calling a function after- adding cycles, or resuming from- awaitwhich reflects a refund.
- func available() : (amount : Nat): Returns the currently available- amountof cycles. This is the amount received from the current caller, minus the cumulative amount- accepted so far by this call. On exit from the current shared function or- asyncexpression via- returnor- throwany remaining available amount is automatically refunded to the caller.
- func accept<system>(amount : Nat) : (accepted : Nat): Transfers- amountfrom- available()to- balance(). It returns the amount actually transferred, which may be less than requested, for example, if less is available, or if canister balance limits are reached. Requires- systemcapability.
- func add<system>(amount : Nat) : (): Indicates the additional amount of cycles to be transferred in the next remote call, i.e. evaluation of a shared function call or- asyncexpression. Upon the call, but not before, the total amount of units- added since the last call is deducted from- balance(). If this total exceeds- balance(), the caller traps, aborting the call. Requires- systemcapability.
- func refunded() : (amount : Nat): Reports the- amountof cycles refunded in the last- awaitof the current context, or zero if no await has occurred yet. Calling- refunded()is solely informational and does not affect- balance(). Instead, refunds are automatically added to the current balance, whether or not- refundedis used to observe them.
Since cycles measure computational resources spent, the value of balance() generally decreases from one shared function call to the next.
The implicit register of added amounts, incremented on each add, is reset to zero on entry to a shared function, and after each shared function call or on resume from an await.
Example
To illustrate, we will now use the ExperimentalCycles library to implement a simple piggy bank program for saving cycles.
Our piggy bank has an implicit owner, a benefit callback and a fixed capacity, all supplied at time of construction. The callback is used to transfer withdrawn amounts.
import Cycles "mo:base/ExperimentalCycles";
shared(msg) persistent actor class PiggyBank(
  benefit : shared () -> async (),
  capacity: Nat
  ) {
  transient let owner = msg.caller;
  var savings = 0;
  public shared(msg) func getSavings() : async Nat {
    assert (msg.caller == owner);
    return savings;
  };
  public func deposit() : async () {
    let amount = Cycles.available();
    let limit : Nat = capacity - savings;
    let acceptable =
      if (amount <= limit) amount
      else limit;
    let accepted = Cycles.accept<system>(acceptable);
    assert (accepted == acceptable);
    savings += acceptable;
  };
  public shared(msg) func withdraw(amount : Nat)
    : async () {
    assert (msg.caller == owner);
    assert (amount <= savings);
    await (with cyles = amount) benefit();
    let refund = Cycles.refunded();
    savings -= amount - refund;
  };
}
The owner of the bank is identified with the implicit caller of constructor PiggyBank(), using the shared pattern, shared(msg). Field msg.caller is a Principal and is stored in private variable owner for future reference. See principals and caller identification for more explanation of this syntax.
The piggy bank is initially empty, with zero current savings.
Only calls from owner may:
- Query the current - savingsof the piggy bank (function- getSavings()), or
- Withdraw amounts from the savings (function - withdraw(amount)).
The restriction on the caller is enforced by the statements assert (msg.caller == owner), whose failure causes the enclosing function to trap without revealing the balance or moving any cycles.
Any caller may deposit an amount of cycles, provided the savings will not exceed capacity, breaking the piggy bank. Because the deposit function only accepts a portion of the available amount, a caller whose deposit exceeds the limit will receive an implicit refund of any unaccepted cycles. Refunds are automatic and ensured by the ICP infrastructure.
Since the transfer of cycles is unidirectional from caller to callee, retrieving cycles requires the use of an explicit callback using the benefit function, taken by the constructor as an argument. Here, benefit is called by the withdraw function, but only after authenticating the caller as owner. Invoking benefit in withdraw inverts the caller/caller relationship, allowing cycles to flow upstream.
Note that the owner of the PiggyBank could supply a callback that rewards a beneficiary distinct from owner.
Here’s how an owner, Alice, might use an instance of PiggyBank:
import Cycles = "mo:base/ExperimentalCycles";
import Lib = "PiggyBank";
actor Alice {
  public func test() : async () {
    Cycles.add<system>(10_000_000_000_000);
    let porky = await Lib.PiggyBank(Alice.credit, 1_000_000_000);
    assert (0 == (await porky.getSavings()));
    Cycles.add<system>(1_000_000);
    await porky.deposit();
    assert (1_000_000 == (await porky.getSavings()));
    await porky.withdraw(500_000);
    assert (500_000 == (await porky.getSavings()));
    await porky.withdraw(500_000);
    assert (0 == (await porky.getSavings()));
    Cycles.add<system>(2_000_000_000);
    await porky.deposit();
    let refund = Cycles.refunded();
    assert (1_000_000_000 == refund);
    assert (1_000_000_000 == (await porky.getSavings()));
  };
  // Callback for accepting cycles from PiggyBank
  public func credit() : async () {
    let available = Cycles.available();
    let accepted = Cycles.accept<system>(available);
    assert (accepted == available);
  }
}
Alice imports the PiggyBank actor class as a library so she can create a new PiggyBank actor on demand.
Most of the action occurs in Alice's test() function:
- Alice dedicates - 10_000_000_000_000of her own cycles for running the piggy bank by calling- Cycles.add(10_000_000_000_000)just before creating a new instance,- porky, of the- PiggyBank, passing callback- Alice.creditand capacity (- 1_000_000_000). Passing- Alice.creditnominates- Aliceas the beneficiary of withdrawals. The- 10_000_000_000_000cycles, minus a small installation fee, are credited to- porky's balance without any further action by the program's initialization code. You can think of this as an electric piggy bank that consumes its own resources as its used. Since constructing a- PiggyBankis asynchronous,- Aliceneeds to- awaitthe result.
- After creating - porky, she first verifies that the- porky.getSavings()is zero using an- assert.
- Alicededicates- 1_000_000of her cycles (- Cycles.add<system>(1_000_000)) to transfer to- porkywith the next call to- porky.deposit(). The cycles are only consumed from Alice’s balance if the call to- porky.deposit()succeeds.
- Alicenow withdraws half the amount,- 500_000, and verifies that- porky's savings have halved.- Aliceeventually receives the cycles via a callback to- Alice.credit(), initiated in- porky.withdraw(). Note the received cycles are precisely the cycles- added in- porky.withdraw(), before it invokes its- benefitcallback- Alice.credit.
- Alicewithdraws another- 500_000cycles to wipe out her savings.
- Alicetries to deposit- 2_000_000_000cycles into- porkybut this exceeds- porky's capacity by half, so- porkyaccepts- 1_000_000_000and refunds the remaining- 1_000_000_000to- Alice.- Aliceverifies the refund amount (- Cycles.refunded()), which has been automatically restored to her balance. She also verifies- porky's adjusted savings.
- Alice's- credit()function simply accepts all available cycles by calling- Cycles.accept<system>(available), checking the actually- acceptedamount with an assert.
For this example, Alice is using her readily available cycles that she already owns.
Because porky consumes cycles in its operation, it is possible for porky to spend some or even all of Alice’s cycle savings before she has a chance to retrieve them.