diff --git a/docs/references/ic-interface-spec/abstract-behavior.md b/docs/references/ic-interface-spec/abstract-behavior.md index ce1ab18..a99ca39 100644 --- a/docs/references/ic-interface-spec/abstract-behavior.md +++ b/docs/references/ic-interface-spec/abstract-behavior.md @@ -509,6 +509,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) @@ -618,6 +619,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 = (); @@ -1014,6 +1016,34 @@ 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) +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. @@ -1666,6 +1696,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: @@ -1730,6 +1764,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 @@ -1812,6 +1847,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: @@ -1868,6 +1907,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 @@ -1902,6 +1942,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]; @@ -2893,6 +2934,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) @@ -3087,6 +3129,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: @@ -3154,6 +3200,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 @@ -4126,6 +4173,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 cdf621d..86e2fcf 100644 --- a/docs/references/ic-interface-spec/changelog.md +++ b/docs/references/ic-interface-spec/changelog.md @@ -16,6 +16,9 @@ sidebar: omitting the field. Requests of any kind carrying a delegation with any other value of the `permissions` field are not accepted. * `wasm_memory_threshold` in canister settings is now bounded by 248, analogously to `wasm_memory_limit`. +* 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.63.0 (2026-06-29) {$0_63_0} * Support for the HTTP method `PATCH` in canister `http_request` in non-replicated mode. diff --git a/docs/references/ic-interface-spec/management-canister.md b/docs/references/ic-interface-spec/management-canister.md index bcf1e95..386f669 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, 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 519d1c0..8d6fab7 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;