Spent a few more hours on this and couldn’t find a flaw.
I was going to point out that contract class data (ie bytecode) may be too big for conventional events. But then I checked how we’re handling it today: we are simply not validating it. And to validate it, it’ll need to go through the same flow as events. Maybe there’s a difference regarding how we’d commit to it…?
The one other thing that bothers me is that, by placing everything in the nullifier tree, which grows a lot faster than contract classes and instances, we may end up with more expensive merkle membership proofs whenever we need to lookup the bytecode to run for a given function. And if we implement nullifier epochs this means that every client will need to store the old frozen trees that include the nullifiers for the contracts they need to run. Keeping contract classes and instances on a separate tree means we probably don’t need epochs for that tree, or at least we change them far less frequently.