# Confused-deputy owner takeover in `fakfun-smart-router-registry` ## Finding **High:** every owner-only entry checks `tx-sender`, including `propose-owner` at line 79. In Clarity, `tx-sender` remains the originating account across nested contract calls. A contract called by the legitimate registry owner can therefore call `propose-owner(attacker)` and pass the owner check even though the immediate `contract-caller` is untrusted. After the 144-block cooldown, the attacker calls `accept-owner` directly. That function requires only that `tx-sender` equal the pending owner, so the attacker becomes registry owner without another transaction or confirmation from the legitimate owner. The same ambient-authority check appears in `propose-router` (line 39), `confirm-router` (line 53), and `revoke-pending` (line 67). ## Deployed-source verification On 2026-09-09, the source returned by Hiro for `SPV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RCJDC22.fakfun-smart-router-registry` matched `contracts/fakfun-smart-router-registry.clar` byte for byte after only normalizing CRLF to LF. Both normalized sources have SHA-256 `c2d62d7a565f22c52fc90795ac48de82e85d7aa2d7ed6cb518dcb2b049668a7a`. Source endpoint: `https://api.hiro.so/v2/contracts/source/SPV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RCJDC22/fakfun-smart-router-registry?proof=0` Stacks documents `tx-sender` as the original transaction sender and `contract-caller` as the immediate caller, which changes on `contract-call?`: `https://docs.stacks.co/reference/clarity/keywords` ## Impact The new registry is the global allowlist authority for every `smart-buy-*` and `smart-sell-*` entry in `fakfun-wallet-v18`. After taking ownership, the attacker can stage and confirm an attacker-controlled router, and approvals are intentionally permanent. Once approved, that router passes the registry gate for every v18 wallet. A wallet trade still needs its own admin or passkey authorization, so registry takeover alone does not move wallet funds. The attack needs one transaction in which the registry owner originates a call to an attacker-controlled or compromised contract. A direct attacker call to `propose-owner` correctly fails with `u7001`; the nested call succeeds because authorization follows ambient `tx-sender` authority. ## Reproduction Run against the exact repository contract: ```powershell npx vitest run tests/fakfun-smart-router-registry-confused-deputy.test.ts -- --manifest tests/cl-v18-registry/Clarinet.toml ``` The test performs this sequence: 1. An attacker directly calls `propose-owner(attacker)` and receives `u7001`. 2. The legitimate owner calls only `zz-owner-proxy.stage-owner-takeover`. 3. The proxy calls the registry's `propose-owner(attacker)`; it succeeds because `tx-sender` is still the legitimate owner. 4. After 144 burn blocks, the attacker calls `accept-owner` and becomes owner. The second test rewrites only the four owner checks to use `contract-caller` in memory. The nested proposal then fails with `u7001`, and `pending-owner` remains `none`. Files: - `tests/fakfun-smart-router-registry-confused-deputy.test.ts` - `tests/cl-v18-registry/Clarinet.toml` - `tests/cl-v18-registry/contracts/zz-owner-proxy.clar` ## Correction Use `contract-caller` for direct authority checks in `propose-owner`, `propose-router`, `confirm-router`, and `revoke-pending`. The owner acceptance check should likewise authenticate the immediate caller so a contract owner can act only through its own code. Add a regression test where the owner calls an intermediary and the intermediary attempts each owner-only operation. This finding is independent of the existing submissions about token-lock, unbound sell-token principals, permanent approvals, stale signatures, the swap extension entry point, and `max-steps`.