A $24K finding from the AAVE Aptos V3 audit on Cantina. The bug was embarrassingly simple — data stored at one address, every getter reading from another. The entire protocol would have been dead on arrival.
AAVE V3 on Aptos wasn't just another deployment. This was AAVE's first-ever expansion beyond EVM-compatible chains — a move from the Solidity world it was born in to Move, a completely different language with a completely different resource model. Aptos, built by former Meta/Diem engineers, offered high throughput and a fresh DeFi ecosystem. AAVE was betting big on it.
When Cantina posted the contest — $150,000 GHO prize pool, 409 auditors competing — I knew the codebase would be interesting. Porting a battle-tested protocol like AAVE to a new language is exactly the kind of work where subtle bugs hide in plain sight. I'd seen this movie before with OpenZeppelin's Cairo contracts, where implicit safety invariants got lost during the port from Solidity.
This time, the bug was even simpler. And the impact was even worse.
If you're coming from Solidity, Move's storage model takes a minute to get used to. In Solidity, contract state lives at the contract's address — you deploy to 0xABC, and your storage lives at 0xABC. Simple.
In Move (Aptos's smart contract language), data is stored as resources under specific account addresses. When you call move_to(account, resource), that resource lives at that account's address. To read it back, you call borrow_global<ResourceType>(address) — and you have to specify the exact same address where it was stored.
Get the address wrong, and the VM throws ERESOURCE_DNE — Resource Does Not Exist. The transaction aborts. No fallback, no default value, just a hard revert.
Keep that in mind.
I was going through the codebase module by module, starting from the data layer — the foundation everything else builds on. In AAVE Aptos, the configuration data (price feeds, underlying assets, reserve configs) lives in a dedicated module called aave-data.
The initialization function was clean and straightforward:
Perfect. The data resource gets initialized and stored at @aave_data. That's where it lives. That's the only place it lives.
Then I went to the getter functions — the functions every other module in the protocol calls to retrieve configuration data. Price feeds, reserve configs, underlying assets — everything flows through these getters.
I stared at this for a good ten seconds before it clicked.
Every single getter function was reading from @aave_pool. But the data was stored at @aave_data. These are two completely different addresses.
This wasn't a subtle edge case. This wasn't a rounding error on the fifteenth decimal place. This was a complete protocol brick.
ERESOURCE_DNE.Think about the context: AAVE is one of the most important protocols in DeFi. Their expansion to Aptos was a marquee moment — governance votes, community excitement, ecosystem partnerships lined up. And the entire deployment would have failed on the first transaction because every getter was looking for data at an address where no data existed.
The proof of concept was almost comically simple. In a contest where PoCs were mandatory, this one took about four lines:
The test passes. The addresses are different. The bug is real. That's it.
Sometimes the most impactful findings have the simplest proofs.
Here's an interesting detail about this finding. After we reported it, the AAVE team confirmed it and shared something worth noting:
"Thanks for this finding. We realized this was wrong in the CI/CD and had it fixed."
— AAVE TeamThe AAVE team had independently caught this through their CI/CD pipeline — but it was discovered after we had already submitted the report. The finding was in scope of the audit, it was valid, and it was confirmed as a High severity issue.
This is worth reflecting on for a moment. Even a team as experienced as AAVE, with automated testing pipelines and multiple prior audits on the EVM side, shipped this to a public contest before their own CI/CD caught it. That's not a knock on them — it's a reminder that cross-language ports introduce subtle mismatches that are easy to miss and hard to catch automatically.
It's also a powerful argument for public audit contests. The crowd found it in parallel with the team's own processes. Redundancy in security isn't wasteful — it's essential.
If you've been reading my previous posts, this should sound familiar. I wrote about the exact same class of issue in OpenZeppelin's Cairo contracts (CVE-2024-45304), where a safety invariant from the Solidity version didn't survive the port to Cairo.
Here's the pattern: when a protocol gets ported from one language to another, the logic gets translated but the implicit assumptions get lost. In Solidity, you don't think about storage addresses because your contract's state lives at your contract's address automatically. In Move, you have to be explicit about it — and that's where the mismatch crept in.
This is now a recurring theme in my findings. Cross-language ports are bug magnets. If you're auditing a protocol that was ported from one language to another, start by mapping every implicit assumption the original language makes, and verify that each one is explicitly handled in the new version.
The fix was exactly what you'd expect — change @aave_pool to @aave_data in every getter function. A global find-and-replace that takes 30 seconds.
That's the thing about bugs like this. The fix is trivial. The impact is catastrophic. And the gap between the two is where $24K payouts live.
AAVE's move to Aptos is genuinely exciting for DeFi. Taking a battle-tested protocol beyond EVM boundaries is the kind of expansion this space needs. But every bridge to a new ecosystem carries risk — not from the business logic being wrong, but from the translation itself introducing cracks.
This finding reinforced something I keep coming back to: read the code from the ground up. Don't start with the flashy stuff — the liquidation engine, the oracle integration, the governance module. Start with the boring stuff. Where is data stored? How is it retrieved? Do the addresses match?
In this case, they didn't. And that one mismatch would have bricked a $150K contest protocol on day one.
The simplest bugs often have the loudest impact. Never skip the boring code. 🔥