Dan Ogurtsov — Web3 research and audits, leading teams of best talents. Personal site with book reviews, articles and a travel log.

Trying My Hand at Open Source: Building Dandelion, an On-Chain Research Tool

I've spent a lot of time over the years poking at things on-chain. And there's one small thing that quietly annoyed me the whole time: you have a contract in front of you, an address on a live chain, and you have no idea what it actually is. No name, no repo, no context. Just bytecode and a block explorer that shows you one transaction at a time.

Block explorers like Etherscan are great at zooming into a single contract or a single transaction. What they never gave me was the shape of the whole thing - which addresses belong together, who controls what, where the money sits. To get that, I'd end up opening ten tabs, following proxy slots and role events by hand, guessing which contract belonged to the project and which was just an outside dependency it happened to call. Slow, and easy to get wrong.

An ink-style dandelion scattering its seeds in the wind

dandelion - the name felt right for a tool that starts from one seed and spreads out.

This itch is old. I even remember, back when I was at P2P.org, we ran internal hackathons, and I was the one organizing one of them. One of the tasks I set was basically this exact problem: take a pile of contracts and cluster them into their projects. I told everyone it didn't have to be perfect - even a statistical guess, "these addresses probably belong to the same thing," would already be useful. It was a fun problem to hand out. Nobody fully cracked it, and honestly I didn't expect them to.

Then I moved into audits and spent a few years there. And in all that time, this particular corner of on-chain analysis never really got better. Identifying a system from addresses alone, drawing a line around where a project starts and ends - it stayed just as manual as I remembered. So at some point I stopped waiting for a tool to show up and built one for myself. I called it dandelion, and I put it out as open source (MIT). It's the first time I'm really trying my hand at open source, and it felt like the right project to do it with.

The idea is simple to say: you give it a set of addresses and a chain, and it hands you back a map. It resolves the code, reads the real on-chain state, follows the links between contracts, works out roles and who controls what, and folds all of that into one typed graph you can actually build research on.


How it works

The thing I like about it is that it isn't a rigid pipeline. Every protocol is different, so you can't know upfront where the search will go. Instead it runs as a loop over a shared graph - the graph is the memory. A deterministic pass gathers hard facts, then (optionally) an AI pass looks at what's there and suggests where to look next, then deterministic probes go check those suggestions, and round it goes until nothing new turns up.

The loop: a deterministic sweep gathers facts, an optional LLM pass suggests where to look next, deterministic probes verify, repeat until it converges

The loop: facts first; the AI pass is optional and only suggests where to look, and every lead gets verified before it lands in the graph.

Most of the tool is really just me working around a few things that kept getting in the way.

The first one was source - half the time there just isn't any. An unverified fork, a closed deployment, a contract someone forked and never published. So the tool tries a ladder of sources - Etherscan, then Sourcify, then Blockscout - and if none of them have it, it decompiles the bytecode, so an unverified contract still gets mapped. It also detects the usual proxy patterns and follows them to the real implementation and admin - the contract sitting at the address is usually just a proxy, with the real code somewhere else.

The next one was that old hackathon problem: which addresses actually belong together? This is the heart of it, and my answer took a while to get right. My first instinct was to follow references - if contract A points at contract B, they're related. But that's wrong. A lending pool reads a price from an oracle, and the oracle isn't part of the project. So I stopped following references and started following control - who admins a contract, who deployed it. The admins, role-holders, timelocks and deployers form a set that closes in on itself: a member's own authorities pull in the next contract, and the next. A contract you merely reference stays outside, and known infrastructure like WETH or USDC is kept outside on purpose, even when the project calls it. Switching from reference to control is what finally made the clustering actually work.

And then there was the AI question: where does it fit without ruining everything? I really didn't want a tool that made things up - one wrong address and the whole map is garbage. So the AI never writes to the graph. It can only suggest reads to run - "go call this getter on that stuck contract" - and a deterministic layer runs each suggestion and checks it before anything is added. If the model is wrong, or someone slips a prompt-injection into a contract name, the worst case is one wasted read. And I measured what it actually buys, instead of assuming: on protocols that have source, the deterministic engine already finds everything, so the model adds basically no new structure - just labels and a summary. It only really helps on the opaque stuff, bytecode with no ABI. No point overselling it beyond that.

Putting it on Aave

My favorite test is Aave, because Aave does not make it easy. You'd think you could point the tool at the main Pool address and be done, but the Pool hides its wiring: its parts are reached through a registry instead of direct links, and its admin is baked into the proxy in a way you can't just read off a standard slot. Point a naive crawler at it and you get two contracts and a dead end.

From one Aave Pool address, getter expansion reaches a 14-node core with 10 members, the oracle stays external, and the thousands of per-reserve tokens fold into one clone-class

One address in, the Aave v3 Pool cluster out - a named 14-node core. The oracle is correctly left outside, and thousands of near-identical reserve contracts collapse into a single class.

What cracks it open is that Aave, like a lot of well-built protocols, exposes its own parts through plain getter functions. Ask the Pool for its addresses provider, ask the provider for the rest - the interest-rate strategy, the logic libraries, the payloads controller, and so on. The tool just calls each getter and follows the address it returns. Two contracts become fourteen, ten of them clearly part of the project - all of it deterministic, no AI needed. Then it walks the reserve list and, for each accepted asset, finds its aToken, debt token and rate strategy. Those run into the thousands, so instead of bloating the map it folds them into a single "clone-class." The price oracle gets reached too, but it's correctly left outside the project, because the Pool only reads from it.

From one address, you end up with a clean, named map of the Aave core: proxies wired to their implementations, the role hubs and admins, the oracle marked as an outside dependency, the reserves tagged and collapsed - and a machine-readable file you can hand to whatever you're doing next. Honestly, that map is the thing I kept wanting for years and never had.

How I tested it

I didn't want to trust my own gut on whether it worked, so I built it against real protocols with a known answer - Aave, Fluid, Morpho, Velodrome, deBridge, Liquity, GMX, mETH - and scored each run against ground truth I put together by hand. Every time something was off, I found the bottleneck, fixed it, and added a test so it wouldn't come back. There's a separate held-out set of protocols the tool was never tuned against, to keep me honest, and it mapped most of them cleanly on the first try. The one real miss so far is Curve, whose Vyper style of exposing its token list the engine doesn't read yet. I left that one in the README as a known miss.

What's next

Two things are on my mind. First, I want it to be nicer to actually use. Right now you clone the repo and pip install it, and I don't love making people do that just to look at one contract:

git clone https://github.com/danogurtsov/dandelion
cd dandelion
pip install -e ".[dev]"

I keep thinking it really wants to be a web thing - paste an address, watch the map build itself - so nobody has to install anything. I haven't built that yet, but it's where my head is.

Second, more testing on projects I've never seen. The whole point is to be useful on the unknown deployment, the weird fork, the thing with no repo - and the only way to trust that is to keep pointing it at unfamiliar addresses and seeing where it breaks.

If any of this sounds fun to you, the code is on GitHub. And if you point it at something and it gets things wrong, I'd genuinely love to hear about it - that's the most useful feedback right now.