Checking that a deployed contract still runs the audited commit

The verified badge on an explorer shows that some source compiles to the code at an address. It doesn’t say which commit that source came from, and on a proxy it says nothing about the logic behind it. So when someone asks whether an upgradeable contract still runs the commit its audit covered, the honest answer is often a guess, and a few upgrades after launch the guess can be wrong.

I’ve been writing down what a real check needs. Most of it is small.

Rebuilding the commit

To tie the address to the report you rebuild the audited commit with the same settings (solc version, optimizer runs, viaIR, evmVersion) and compare runtime bytecode.

If the contract has immutables, it won’t match byte for byte. The constructor writes their values into the runtime code, so a fresh build has zeros where the deployed code has an oracle address or a fee. solc gives you the positions in evm.deployedBytecode.immutableReferences in the standard JSON output. Zero those ranges on the deployed side, compare, and then check the immutable values on their own. What this basically means is that a clean build with the wrong oracle baked in still passes the bytecode comparison.

The end of the bytecode is the other place it breaks. solc appends the IPFS hash of the metadata file, and the metadata holds a hash of every source file, so a changed comment changes the tail. I tried it on a small contract with one immutable: adding one comment line changed exactly the 32 bytes of that hash and nothing else. If everything matches except the tail, the compiled logic is the same but the sources are not the ones in the report. Sourcify calls the two cases “exact match” and “match”, and it’s worth knowing which one you have.

Where the implementation is

On a proxy, read the EIP-1967 implementation and admin slots. If the beacon slot is set instead, the code sits behind the beacon’s implementation(). One upgrade on the beacon changes every proxy that uses it, and the Upgraded event comes from the beacon, not from the proxies.

Older proxies don’t use those slots. ZeppelinOS proxies keep the address at keccak256(“org.zeppelinos.proxy.implementation”), with no minus one. EternalStorage-style proxies keep it in a normal storage variable behind an implementation() getter, and their event is Upgraded(uint256 version, address implementation). That’s a different topic hash from the EIP-1967 event, so a log filter on the standard topic never fires for them. A Diamond has no single implementation, you read the facet list through facets() and watch DiamondCut.

7702 accounts go on the same list now. The account’s code is 0xef0100 followed by the delegate address, and the code that runs is the delegate’s. The account can point it somewhere else with one signed authorization that anyone can submit, and EIP-7702 defines no event for that, so you only see it by reading the account’s code.

It also helps to map early which contracts share one admin. The report scopes a list of contracts. The ProxyAdmin or multisig on chain can upgrade more than that list.

What a monitor counts as a change

Watching the implementation slot catches a swap. Admin and owner changes matter as much, since they decide who can do the next upgrade, and so do the parameters the report treated as fixed, like fee caps, oracle addresses or who can pause.

Upgrades can also happen and be undone inside one transaction. When I traced Arbitrum’s 30 October freeze for an earlier topic here, the Inbox implementation slot read 0x7c05…0a10 at block 26301885 and the same value at 26301886. In between, the transaction in that block pointed the Inbox at other code and back, twice. Only the four Upgraded logs show it, so a monitor needs logs as well as state reads.

A read that fails is not a read that matched. A timeout, an error from a node that pruned the block you asked for, or a node a few blocks behind should come back as unknown. If errors count as no change, the monitor stays green on exactly the day it was built for. Pin each read to a block number and compare the head across two providers.

If you run an upgradeable contract that was audited, what do your users have today to check that the address still runs the audited commit?