Let me make sure I’m reading your position precisely before pushing back, so I’m addressing what you actually said.
As I understand it, your model has two levels:
- At the DAO level: reject all ingress and accept only inter-canister calls with sufficient cycles attached. Enforceable today via
canister_inspect_message + a cycles check in the method body.
- At the ecosystem level: if everyone routes through wallet canisters, the only thing ever receiving ingress on the whole IC is the wallet wasm. That’s “1 wasm” to harden, instead of every canister defending its own ingress surface individually.
The DAO-level part I’ll concede: enforcing “caller is a canister and has attached enough cycles” is straightforward, and it does shift cost from defender to attacker. That’s real and useful against low-value spam.
The ecosystem-level “1 wasm” claim is where I think the argument doesn’t hold, for three concrete reasons:
-
There’s no enforcement that callers use the hardened wallet wasm. The DAO sees inter-canister + cycles attached and accepts. It can’t distinguish “call from the blessed hardened wallet” from “call from a custom canister an attacker deployed five minutes ago and funded with cycles”. Verifying the caller’s wasm synchronously from inside a method body isn’t feasible; the System API gives you msg_caller, not module_hash. Getting the hash requires an async canister_info round-trip whose cycles are paid by the defender, with TOCTOU between the check and the in-flight message (the IC’s message envelope doesn’t carry the sender’s module_hash, and an attacker can install_code between your canister_info call and the actual message). So the protection the DAO actually gets is economic gating via attached cycles, full stop. The “1 wasm hardening” doesn’t add a layer on top of that; the cycles-attached check already does all the enforceable work.
-
Cost-shift alone doesn’t deter high-value attacks. Yes, the attacker pays from their canister instead of the defender paying. But for attacks targeting high-value moments, e.g. blocking a critical governance proposal, stalling a ledger during volatility, freezing a DEX during an arbitrage window, the cost of running an attack canister for a few hours is rationally bearable. The economic flip protects against unmotivated spam, not against motivated adversaries on time-sensitive targets.
-
The “1 wasm” coordination doesn’t exist today, and forcing it back regresses the wallet ecosystem. Today’s IC wallets delegation flow don’t route calls to dapps as inter-canister calls from a wallet canister. They sign and deliver ingress messages directly to the target canister, whether signing happens in a browser extension, a passkey-backed web app, local storage in a web app, or a tECDSA-backed canister popup signer. In every case the target dapp receives a user-principal ingress, not a wallet-canister ICC.
At the protocol level there is no association between an Internet Identity anchor and any wallet canister. The IC interface spec defines five principal classes distinguished by the trailing byte of their binary representation: opaque ids (terminating in 0x01, assigned by the IC to canisters), self-authenticating ids (form H(public_key) · 0x02, used by external users who hold the private key), derived ids (0x03), anonymous (0x04), and reserved (0x7f). When an II user calls a dapp, the principal that arrives at caller() is a self-authenticating id derived from the user’s delegated session key: H(UserKey) · 0x02. It is structurally distinct from a canister principal. A target canister can trivially verify the class by inspecting the last byte of msg_caller().as_slice().
For your model to be enforceable the target must reject every principal whose last byte is not 0x01. That excludes every II user, every NFID user, every OISY user (the popup signer still delivers a self-authenticating principal, not an ICC from the OISY canister), every Plug/Stoic/Bitfinity user, and every dfx identity from CLI. To call the dapp, a user would need to deploy a personal canister, fund it with cycles, implement their own authentication on top, and route every call through it. That is the cycles-wallet pattern, and DFINITY has been moving away from it on both fronts: cycle management has been replaced by the cycles ledger, and user authentication has been replaced by II + direct ingress. prepare_delegation / get_delegation in the II Candid expose no concept of “the user’s wallet canister” because none exists at the protocol level.
Even granting that migration, “one wasm” means imposing a single canonical implementation on the user base. Different threat models call for different wallets (multi-sig, hardware-backed, custodial, social recovery, policy-restricted). Forks, security patches, and version updates each produce a new hash; there’s no on-chain mechanism to pin “the blessed one” without forklocking wallet innovation.
So the model as proposed gives the DAO real economic protection, but that’s it. The “1 wasm” abstraction is a coordination assumption that doesn’t hold today, wouldn’t be enforceable even if it did, and doesn’t add anything on top of the cycles-attached check that’s already doing the actual gating.
On “where does your contention come from?”
From the SNS governance canister DFINITY itself ships. In rs/sns/governance/canister/canister.rs (master, commit 5458e72, last edited 2026-03-17), lines 397–398:
#[update]
async fn manage_neuron(request: ManageNeuron) -> ManageNeuronResponse { ... }
It’s a standard #[update]. There is no canister_inspect_message anywhere in canister.rs or neuron.rs (grep returns zero matches in either file), and no canister-only restriction on the caller. Authorization is purely principal-based via NeuronPermission: is_authorized is defined in rs/sns/governance/src/neuron.rs at line 125, and called at line 162 as sufficient_permissions.iter().any(|p| self.is_authorized(caller, *p)). caller is the value returned by the message’s caller() it can be an II principal, an NFID principal, or any other principal type. SNS does not discriminate.
So the DAO model DFINITY actually ships and operates accepts ingress from authenticated users directly. If wallet-canister-only were the right answer, every SNS DAO today would be mis-designed.
Hotkey-style delegation via AddNeuronPermissions exists, but it’s opt-in, not a requirement, and even then the SNS canister still accepts the delegate’s ingress directly. The attack surface doesn’t move.
On the broader architectural implication
The default fee model on ICP is explicit in the current docs: “Cycles are paid by the canister, not the caller: developers fund their own canisters, and users interact for free” (from docs.internetcomputer.org/guides/canister-management/cycles-management). Attaching cycles to a call is the exception, and DFINITY itself documents the proxy-canister pattern as the way to attach cycles when calling from ingress: “if you need to call a canister method with cycles attached from the CLI or an external agent, deploy a proxy canister” (from docs.internetcomputer.org/concepts/cycles).
Proposing universal wallet-canister intermediation as a defensive pattern is effectively proposing to flip that default for any canister that wants to be protected, and to push users back through an intermediary they have to fund themselves. That’s a much larger architectural conversation than cycle-drainage mitigation, and it carries the same UX regression that pushed DFINITY away from the cycles-wallet flow toward II + direct ingress in the first place.
Long story short:
- The DAO-level enforceable protection in your model is economic (cycles attached at the inter-canister boundary). The “1 wasm” coordination doesn’t add anything on top of that and isn’t enforceable today.
- Cost-shift via cycles-attached protects against spam but not against motivated adversaries hitting high-value moments; the asymmetry between attacker cost and defender damage still matters.
- The IC’s principal model itself (0x01 canisters vs 0x02 self-authenticating users) makes the model’s gate trivially implementable but draconian: enforcing it means rejecting every II / NFID / OISY / Plug / dfx-CLI user and requiring everyone to deploy and fund a personal canister; i.e., the cycles-wallet pattern that DFINITY’s own protocol evolution (II + direct ingress, cycles ledger) was designed to replace.
- DFINITY’s own SNS DAO design is a working counter-example to the claim that DAOs can be wallet-canister-gated by default.
If the path forward is “make cycles-attached ingress the new default at the protocol layer”, I’d genuinely welcome that as a Mission-70-era roadmap conversation, but that’s protocol work, not application-level patchwork each canister team has to reinvent.