From 46ebefa85694deb6cfb38756266a74c164cc7406 Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Wed, 24 Jun 2026 09:54:23 +0000 Subject: [PATCH 1/2] feat: new canister settings minimum_incoming_canister_call_cycles --- .../ic-interface-spec/abstract-behavior.md | 50 +++++++++++++++++++ .../references/ic-interface-spec/changelog.md | 3 ++ .../ic-interface-spec/management-canister.md | 8 +++ public/references/ic.did | 2 + 4 files changed, 63 insertions(+) diff --git a/docs/references/ic-interface-spec/abstract-behavior.md b/docs/references/ic-interface-spec/abstract-behavior.md index 0961d2e4..3c9497a9 100644 --- a/docs/references/ic-interface-spec/abstract-behavior.md +++ b/docs/references/ic-interface-spec/abstract-behavior.md @@ -508,6 +508,7 @@ S = { balances: CanisterId ↦ Nat; reserved_balances: CanisterId ↦ Nat; reserved_balance_limits: CanisterId ↦ Nat; + minimum_incoming_canister_call_cycles: CanisterId ↦ Nat; wasm_memory_limit: CanisterId ↦ Nat; wasm_memory_threshold: CanisterId ↦ Nat; environment_variables: CanisterId ↦ (Text ↦ Text) @@ -617,6 +618,7 @@ The initial state of the IC is balances = (); reserved_balances = (); reserved_balance_limits = (); + minimum_incoming_canister_call_cycles = (); wasm_memory_limit = (); wasm_memory_threshold = (); environment_variables = (); @@ -1012,6 +1014,35 @@ messages = Older_messages · Younger_messages · ``` +#### Calls with insufficient cycles are rejected + +An inter-canister call from a different canister with fewer cycles attached than the callee's minimum is automatically rejected. + +Conditions + +```html + +S.messages = Older_messages · CallMessage CM · Younger_messages +(CM.queue = Unordered) or (∀ CallMessage M' | FuncMessage M' ∈ Older_messages. M'.queue ≠ CM.queue) +S.canisters[CM.callee] ≠ EmptyCanister +CM.origin = FromCanister _ +CM.caller ≠ CM.callee +CM.transferred_cycles < S.minimum_incoming_canister_call_cycles[CM.callee] +``` + +State after: + +```html + +messages = Older_messages · Younger_messages · + ResponseMessage { + origin = CM.origin; + response = Reject (CANISTER_ERROR, ); + refunded_cycles = CM.transferred_cycles; + } + +``` + #### Call context creation {#call-context-creation} Before invoking a heartbeat, a global timer, or a message to a public entry point, a call context is created for bookkeeping purposes. For these invocations the canister must be running (so not stopped or stopping). Additionally, these invocations only happen for "real" canisters, not the IC management canister. @@ -1664,6 +1695,10 @@ if A.settings.reserved_cycles_limit is not null: New_reserved_balance_limit = A.settings.reserved_cycles_limit else: New_reserved_balance_limit = 5_000_000_000_000 +if A.settings.minimum_incoming_canister_call_cycles is not null: + New_minimum_incoming_canister_call_cycles = A.settings.minimum_incoming_canister_call_cycles +else: + New_minimum_incoming_canister_call_cycles = 0 if A.settings.wasm_memory_limit is not null: New_wasm_memory_limit = A.settings.wasm_memory_limit else: @@ -1728,6 +1763,7 @@ S' = S with balances[Canister_id] = New_balance reserved_balances[Canister_id] = New_reserved_balance reserved_balance_limits[Canister_id] = New_reserved_balance_limit + minimum_incoming_canister_call_cycles[Canister_id] = New_minimum_incoming_canister_call_cycles wasm_memory_limit[Canister_id] = New_wasm_memory_limit wasm_memory_threshold[Canister_id] = New_wasm_memory_threshold environment_variables[Canister_id] = New_environment_variables @@ -1810,6 +1846,10 @@ if A.settings.reserved_cycles_limit is not null: New_reserved_balance_limit = A.settings.reserved_cycles_limit else: New_reserved_balance_limit = S.reserved_balance_limits[A.canister_id] +if A.settings.minimum_incoming_canister_call_cycles is not null: + New_minimum_incoming_canister_call_cycles = A.settings.minimum_incoming_canister_call_cycles +else: + New_minimum_incoming_canister_call_cycles = S.minimum_incoming_canister_call_cycles[A.canister_id] if A.settings.wasm_memory_limit is not null: New_wasm_memory_limit = A.settings.wasm_memory_limit else: @@ -1866,6 +1906,7 @@ S' = S with balances[A.canister_id] = New_balance reserved_balances[A.canister_id] = New_reserved_balance reserved_balance_limits[A.canister_id] = New_reserved_balance_limit + minimum_incoming_canister_call_cycles[A.canister_id] = New_minimum_incoming_canister_call_cycles wasm_memory_limit[A.canister_id] = New_wasm_memory_limit wasm_memory_threshold[A.canister_id] = New_wasm_memory_threshold environment_variables[A.canister_id] = New_environment_variables @@ -1900,6 +1941,7 @@ canister_status(S, Canister_id) = memory_allocation = S.memory_allocation[Canister_id]; freezing_threshold = S.freezing_threshold[Canister_id]; reserved_cycles_limit = S.reserved_balance_limit[Canister_id]; + minimum_incoming_canister_call_cycles = S.minimum_incoming_canister_call_cycles[Canister_id]; wasm_memory_limit = S.wasm_memory_limit[Canister_id]; wasm_memory_threshold = S.wasm_memory_threshold[Canister_id]; environment_variables = S.environment_variables[Canister_id]; @@ -2891,6 +2933,7 @@ S with balances[A.canister_id] = (deleted) reserved_balances[A.canister_id] = (deleted) reserved_balance_limits[A.canister_id] = (deleted) + minimum_incoming_canister_call_cycles[A.canister_id] = (deleted) wasm_memory_limit[A.canister_id] = (deleted) wasm_memory_threshold[A.canister_id] = (deleted) on_low_wasm_memory_hook_status[A.canister_id] = (deleted) @@ -3085,6 +3128,10 @@ if A.settings.reserved_cycles_limit is not null: New_reserved_balance_limit = A.settings.reserved_cycles_limit else: New_reserved_balance_limit = 5_000_000_000_000 +if A.settings.minimum_incoming_canister_call_cycles is not null: + New_minimum_incoming_canister_call_cycles = A.settings.minimum_incoming_canister_call_cycles +else: + New_minimum_incoming_canister_call_cycles = 0 if A.settings.wasm_memory_limit is not null: New_wasm_memory_limit = A.settings.wasm_memory_limit else: @@ -3152,6 +3199,7 @@ S' = S with balances[Canister_id] = New_balance reserved_balances[Canister_id] = New_reserved_balance reserved_balance_limits[Canister_id] = New_reserved_balance_limit + minimum_incoming_canister_call_cycles[Canister_id] = New_minimum_incoming_canister_call_cycles wasm_memory_limit[Canister_id] = New_wasm_memory_limit wasm_memory_threshold[Canister_id] = New_wasm_memory_threshold environment_variables[Canister_id] = New_environment_variables @@ -4124,6 +4172,8 @@ S with reserved_balances[Canister_id] = (deleted) reserved_balance_limits[New_canister_id] = S.reserved_balance_limits[Canister_id] reserved_balance_limits[Canister_id] = (deleted) + minimum_incoming_canister_call_cycles[New_canister_id] = S.minimum_incoming_canister_call_cycles[Canister_id] + minimum_incoming_canister_call_cycles[Canister_id] = (deleted) wasm_memory_limit[New_canister_id] = S.wasm_memory_limit[Canister_id] wasm_memory_limit[Canister_id] = (deleted) wasm_memory_threshold[New_canister_id] = S.wasm_memory_threshold[Canister_id] diff --git a/docs/references/ic-interface-spec/changelog.md b/docs/references/ic-interface-spec/changelog.md index 118a5c85..f6411fdb 100644 --- a/docs/references/ic-interface-spec/changelog.md +++ b/docs/references/ic-interface-spec/changelog.md @@ -8,6 +8,9 @@ sidebar: ## Changelog {#changelog} +### 0.63.0 (2026-06-24) {$0_63_0} +* New canister setting `minimum_incoming_canister_call_cycles`: if set, inter-canister calls from a different canister that attach fewer cycles than this threshold are rejected with `CANISTER_ERROR` and all attached cycles are refunded. Ingress messages and self-calls are not affected. + ### 0.62.0 (2025-05-26) {$0_62_0} * Inter-canister response callback messages might still be executed after the condition for `canister_on_low_wasm_memory` is triggered and before the function `canister_on_low_wasm_memory` is executed. diff --git a/docs/references/ic-interface-spec/management-canister.md b/docs/references/ic-interface-spec/management-canister.md index db9f000d..d7caa7cf 100644 --- a/docs/references/ic-interface-spec/management-canister.md +++ b/docs/references/ic-interface-spec/management-canister.md @@ -77,6 +77,12 @@ The optional `settings` parameter can be used to set the following settings: Default value: 5_000_000_000_000 (5 trillion cycles). +- `minimum_incoming_canister_call_cycles` (`nat`) + + Must be a number between 0 and 2128-1, inclusively, and indicates the minimum number of cycles that must be attached to an incoming inter-canister call. If a different canister makes a call with fewer attached cycles than this threshold, the call is rejected with `CANISTER_ERROR` and all attached cycles are refunded to the caller. Ingress messages and self-calls (where the caller and the callee are the same canister) are not subject to this limit. + + Default value: 0 (no minimum enforced). + - `wasm_memory_limit` (`nat`) Must be a number between 0 and 248-1 (i.e., 256TB), inclusively, and indicates the upper limit on the WASM heap memory consumption of the canister in bytes. @@ -252,6 +258,8 @@ Indicates various information about the canister. It contains: - The reserved cycles limit of the canister, i.e., the maximum number of cycles that can be in the canister's reserved balance after increasing the canister's memory allocation and/or actual memory usage. + - The minimum number of cycles required for incoming inter-canister calls from a different canister (self-calls and ingress messages are not subject to this limit). + - The visibility of the canister's logs. - The visibility of the canister's snapshots. diff --git a/public/references/ic.did b/public/references/ic.did index 734ee61f..97774b45 100644 --- a/public/references/ic.did +++ b/public/references/ic.did @@ -25,6 +25,7 @@ type canister_settings = record { memory_allocation : opt nat; freezing_threshold : opt nat; reserved_cycles_limit : opt nat; + minimum_incoming_canister_call_cycles : opt nat; log_visibility : opt log_visibility; snapshot_visibility : opt snapshot_visibility; wasm_memory_limit : opt nat; @@ -38,6 +39,7 @@ type definite_canister_settings = record { memory_allocation : nat; freezing_threshold : nat; reserved_cycles_limit : nat; + minimum_incoming_canister_call_cycles : nat; log_visibility : log_visibility; snapshot_visibility : snapshot_visibility; wasm_memory_limit : nat; From 6f58583982b1806388d8f1a4472efaa33cb95123 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:43:28 +0200 Subject: [PATCH 2/2] Update docs/references/ic-interface-spec/abstract-behavior.md --- docs/references/ic-interface-spec/abstract-behavior.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/references/ic-interface-spec/abstract-behavior.md b/docs/references/ic-interface-spec/abstract-behavior.md index bf0eaeb6..a99ca398 100644 --- a/docs/references/ic-interface-spec/abstract-behavior.md +++ b/docs/references/ic-interface-spec/abstract-behavior.md @@ -1026,7 +1026,6 @@ Conditions S.messages = Older_messages · CallMessage CM · Younger_messages (CM.queue = Unordered) or (∀ CallMessage M' | FuncMessage M' ∈ Older_messages. M'.queue ≠ CM.queue) -S.canisters[CM.callee] ≠ EmptyCanister CM.origin = FromCanister _ CM.caller ≠ CM.callee CM.transferred_cycles < S.minimum_incoming_canister_call_cycles[CM.callee]