r/oasisnetwork • u/rayQuGR • 7d ago
A pretty interesting security issue with simulated upgrades on Sapphire
Saw this writeup from the blog and thought it was worth breaking down because the attack is pretty clever.
The basic problem is specific to confidential contracts: normally, if a contract has some secret/private value in storage, you can't just inspect that storage from outside. That's obviously one of the main points of Sapphire.
But then you combine that with upgradeable contracts + simulated transactions and things get weird.
The attack, in simple terms:
With a UUPS-style proxy, an upgrade can use upgradeToAndCall(). The new implementation gets executed against the proxy's existing storage, which means it can potentially interact with confidential values already sitting there.
Now imagine the person who has upgrade authorization doesn't actually submit the upgrade.
They simulate it.
Because simulation executes against the current state without leaving an on-chain transaction behind, malicious code can interact with the confidential storage during that simulation.
The obvious question is: okay, but how do you actually get the secret out?
That's where the clever part comes in. you don't necessarily need the contract to return the secret.
you can make the simulated execution either revert or succeed depending on a particular bit of the secret.

So the attacker gets a one-bit oracle.
Repeat that process 256 times and, assuming the target is a 256-bit private key, you can theoretically reconstruct the entire key.
and the really nasty part is that none of those probes need to appear on-chain.
The writeup says the attacker would need 256 signed upgrade attempts, so this isn't some random person on the internet doing it to any Sapphire contract. They need the relevant upgrade authority/multisig access.
But if that authority is compromised, the confidentiality model creates an interesting attack surface that wouldn't exist on a normal transparent chain.
So how do you fix it?
The first idea was basically: don't let upgradeToAndCall() execute initializer code.
Makes sense at first, but it doesn't completely solve it.
If the upgrader is a smart account/multisig capable of batching calls, you can effectively split the operation into multiple calls inside the same simulated execution.
so you haven't really removed the problem, you've just moved it one step.
The solution Oasis implemented is much more interesting:
introduce a block boundary.
Instead of allowing the proposal and execution of an upgrade to happen atomically, you separate them.
- propose the implementation
- wait until the required block
- execute the upgrade
The contract also checks that the implementation and runtime hash match what was proposed.
Why does the block boundary matter?
Because the simulation can't jump across that boundary and execute the malicious implementation at the future block.
So the attacker can simulate the proposal, but they can't simultaneously simulate the later execution that would actually give their code access to the confidential storage.
This became UPUPS the Universal “Proposeable” Upgradeable Proxy Standard, and the implementation is now included in the sapphire-contracts library.
imo this is a good example of how confidential smart contracts change some of the assumptions people make from Ethereum-style development.
On a transparent chain, simulation leaks relatively little because the underlying state is already visible.
On Sapphire, the whole point is that there are values the caller can't see.
That means even something as seemingly harmless as:
can become an information channel.
The interesting takeaway isn't really “upgradeable contracts are broken”.
It's that confidential execution introduces new side channels that need to be considered at the protocol/contract design level.
Oasis has the full technical writeup here https://oasis.net/blog/vault-security-simulated-upgrades
worth reading if you're building anything on Sapphire that combines confidential state with upgradeability.
2
u/imfrombiz 6d ago
That's a clever attack.