On this page
Abstract
A deployed Solana program is one ELF file, and most of its code is not the author’s. The runtime entrypoint, the serialisation helpers, the allocator, the panic machinery and the token-program bindings are all linked in from the SDK, and a detector that reports a missing signer check inside the SDK’s own account deserialiser is wasting the user’s time. So the analysis partitions functions by provenance, author versus linked runtime, and several detectors only fire on the author’s side. This article is about what happens when that partition is wrong. On stripped mainnet binaries the previous rule, which used the extent of the entrypoint symbol to bound the runtime, claimed between 96 and 99.6 percent of every program as SDK code. Raydium’s AMM had four author-attributed functions out of 348, and none of them contained a multiply or a divide. The replacement rule reads a record the Rust compiler leaves in the binary at every panic, assert and unwrap site, which carries the source path of the code that would panic. That path is a positive proof of authorship. The rule is deliberately asymmetric: it only ever withholds a demotion to the runtime, never promotes on absence, so a program that happens to carry no such records keeps its previous partition unchanged.
What went wrong with the old rule
The old rule was reasonable on the fixtures it was written against. Build a program from source, keep symbols, and the entrypoint symbol’s extent in the ELF is a decent proxy for “the runtime’s dispatch and everything it pulls in”. Functions inside that extent are demoted; functions outside are the author’s.
Stripped mainnet binaries have no such symbol, and the fallback bounded nothing. The extent degenerated to “almost everything”, and the partition followed it. Measured across seven mainnet snapshots the author side was 0.4 to 4 percent of functions. On Raydium, the four functions that survived as author code were four functions that no DeFi detector cares about: the arithmetic the detectors are built to inspect was all on the runtime side, and therefore invisible to them.
That is a quiet failure. Nothing crashes, no test fails, and the detectors report a clean program. The only symptom is that a family of detectors which fires readily on fixtures never fires on mainnet, and that pattern can be explained away for a long time.
The record rustc leaves behind
When Rust code panics, the panic message includes a file path, a line and a column. That information has to come from somewhere, and where it comes from is a per-site record the compiler monomorphises into the binary: a Location value holding a pointer to the file path string, the path’s length, and the line and column. Every panic!, every assert!, every .unwrap() and every arithmetic overflow check in debug configurations gets one.
The record is loaded by a 64-bit immediate instruction whose operand stays in the text section, so it survives stripping; symbols are metadata, the immediate is code. And the path it points at is the author’s own path as the compiler saw it. Raydium’s pricing functions carry the literal string program/src/math.rs. The SDK’s functions carry paths into the cargo registry. A function whose panic record names a path in the program’s own tree was written by the program’s author. That is not a heuristic about layout; it is the compiler telling you where the code came from.
The rule, then: walk each function, find the immediates that resolve to a panic location record, decode the path, and if the path is not a registry path, the function is the author’s. Functions with no panic sites get no anchor and are left exactly as the old rule classified them.
Only ever withholding
That last clause is the design decision we most want to record. The rule never promotes a function to author code on the absence of an SDK path. It only refuses to demote a function that carries a positive author path. A binary with no decodable records at all, for whatever reason, keeps its partition unchanged.
The reason is that silence is not evidence. There are several ways for a function to carry no anchor: it never panics, it was built with a toolchain that lays the record out differently, it was built before the record existed. None of those says anything about who wrote it. A rule that promoted on silence would be making the same class of mistake the old rule made, in the other direction, and the measured consequence of that mistake is in the previous section.
There is a second, quieter reason. A rule that only withholds a demotion cannot make the detectors’ precision worse. It can only expose more author code to them, and whether that code trips a detector is the detector’s business. That property is what let the change land without re-validating every DeFi detector’s false-positive rate first.
Results
Author-attributed functions before and after, on the mainnet snapshots:
| Program | Total functions | Before | After |
|---|---|---|---|
| Raydium AMM | 348 | 4 | 123 |
| Metaplex Token Metadata | 1,138 | 5 | 159 |
| SPL Governance | 474 | 3 | 144 |
| Wormhole | 302 | 2 | 54 |
| Associated Token Account | 71 | 2 | 11 |
| SPL Token | 45 | 2 | 3 |
| Memo | 12 | 6 | 6 |
Memo is unchanged for an instructive reason: it is a 2019 build, and its panic ABI predates the record shape the decoder reads. The rule found no anchors and left the partition alone, which is what it is supposed to do. SPL Token moved by one; it is a small program whose author code has very few panic sites.
On the 170 source-built fixtures, 20 functions moved out of the runtime partition, and every fixture still recompiles, which is the round-trip gate the decompiler is held to.
Three ways to get zero
Decoding the record is a page of code, and three details in that page each produce zero anchors when wrong, with no error. They are worth listing because each cost a wrong answer before it cost the right one.
The record holds a pointer, so it does not live in .rodata with the string it points at. It lives in .data.rel.ro, the section for constants that need relocation. Looking in .rodata finds the path strings and none of the records.
The relocation type is R_BPF_64_RELATIVE, and its implicit addend is not at the relocation offset; it is at the relocation offset plus four, split across the two halves of the 64-bit immediate. Reading it at the offset yields a garbage pointer that resolves nowhere.
Newer compilers include the trailing NUL in the recorded path length; older ones do not. A decoder that assumes one convention silently mis-terminates the string under the other, and a path that ends one byte early or late fails the registry-path test.
Each of these is unit-tested against a hand-built record, because the integration test, “does Raydium have more than four author functions”, cannot tell which of the three went wrong.
Checking that the gate still holds
The DeFi arithmetic detectors run behind a precision gate: a function has to look like pricing or accounting code before its multiplies and divides are inspected, and that gate was designed while provenance was wrong. Exposing 119 more Raydium functions to it was the moment to check whether the gate’s precision had been resting on the provenance rule all along.
The thirteen mainnet functions that carry both a dynamic multiply and a divide are the designated falsifiers. Ten of them are now attributed to the program. The gate qualifies none of the thirteen, before or after, so its precision never rested on the provenance claim; it rested on its own predicates. That is the answer we wanted, and it is the kind of answer that only exists because someone wrote the thirteen down as the functions to watch before the change was made.
The census test that recorded the old gap was also inverted. It had asserted that reachable functions outnumber author functions by more than ten to one, which documented the defect; it now asserts the opposite, so a regression that stops finding anchors fails the test rather than being averaged away.
What it does not do
The rule attributes functions that contain a panic site. A function with no panic, assert, unwrap or checked arithmetic carries no record, and inherits whatever the old rule said. On a typical DeFi program that still leaves a substantial minority of author functions unattributed, and the numbers above are lower bounds on author code, not estimates of it.
It also says nothing about which author. A program that vendors a library into its own tree will show that library’s functions under its own path, because that is literally where the compiler found them. Provenance here means “compiled from the program’s tree”, not “written by the program’s team”.
And it depends on the compiler continuing to emit the record. A future toolchain that strips locations in release builds, or a program built with panic_immediate_abort, would present as the Memo case: no anchors, partition unchanged, detectors back to the old blind spot. The decoder’s unit tests would still pass. The integration census is the only thing that would notice, which is one more reason it asserts a floor rather than a ceiling.