The canister_inspect_message is not invoked for query calls, inter-canister calls or calls to the management canister.
So I’m a bit puzzled, as this seems like a gaping hole for a cycles burn attack, even if I trap on the first line of a call, I’m still paying the base-fee = 5M cycles.
I have an application in mind where the user pays for usage, but seems a bit pointless if any malicious user can spin up a canister and burn cycles by spamming calls anyway.
If this is true, all I can say is, please give us the ability to use inspect_message on inter-canister calls (and query calls as well, if (when) queries become chargeable), as this is untenable.
You’re not wrong to question it, but it’s not quite the “gaping hole” it might look like at first glance.
Yes, canister_inspect_message doesn’t run for inter-canister or query calls, and you still pay the base cost for ingress messages—but that’s kind of by design. The model assumes that canisters interacting with other canisters are already part of a trust or incentive system, rather than completely untrusted spam sources.
For actual abuse scenarios:
A malicious actor still has to spend cycles on their side to generate traffic
You can mitigate by designing your canister to fail fast and cheap (minimal execution before reject)
Rate-limiting, access control, or requiring some form of prepaid / whitelisted interaction can help
So it’s less “unprotected” and more that the responsibility shifts to canister design and economic incentives rather than protocol-level filtering in those cases.
That said, your point is valid—having inspect_message for inter-canister calls (and eventually queries) would give developers much finer control and reduce unnecessary cycle burn. It’s a reasonable feature request, especially for pay-per-use models.
If my napkin math is correct, looks like about 360K cycles for them to spend 5M of mine. Doesn’t seem like a winning situation to be in.
The model assumes that canisters interacting with other canisters are already part of a trust or incentive system, rather than completely untrusted spam sources.
responsibility shifts to canister design and economic incentives
Sure, if there was some way to gate to canisters I trust. But there isn’t. What canister design options do I have? Or am I missing something?
One thing to nuance. If the call comes from an inter-canister call, the attacker’s canister must also be executing. So they also pay the 5M base fee on their side, plus 260K for the xnet call. So it’s closer to 5.3M vs 5M (not counting the instruction cost on their side), a bit more balanced. Also if the canister is not in your subnet you have additional protection (xnet call cost).
Edit: Actually, this might be unbalanced again if as an attacker you use notify/one-way calls to skip callbacks and you batch multiple calls to an external canister inside one update call on your side. Then you pay 5M once + N×260K, while the victim pays N×5M. Back to ~20:1 at scale. I have to admit, I haven’t thought through this scenario before.
This is exactly why execution-system design matters.
If anti-abuse is pushed up to the application layer, then prepaid paths, rate limits, and cheap-fail execution become part of core infra — not optional app logic.
Even if an attacker sends multiple calls in parallel, each call will have its own (response) callback and there will be a base cost of 5M per callback (cycles for instructions are prepaid before making each of the calls - otherwise the calls fail to be sent - so they are available for every response callback). So the cost of parallel cost does not amortize the way you described it above.
This is very unexpected, but looking at the source I do see it, update_message_execution_fee (5M) is charged along with instructions_to_cycles(max_num_instructions). The instruction cycles are refunded as they are not used, but the update_message_execution_fee is not. I’m assuming this is intended, since you referenced it, but it’s not documented, and isn’t intuitively what I would expect sending a Call::oneway(). Is it there for this reason?
A way to control which canisters can send me inbound messages (inspect_message or something else) is still needed, because even if they burn at the same rate as me, it’s something I can’t control, and a DoS unless I am willing to spend to keep up with it.
The reason is that one-way calls are not natively supported by the IC and thus the CDK “emulates” them by passing an invalid function pointer which results in a trap.