Solv Protocol Lost $2.7M to Reentrancy. The Contract Was Unaudited.

On March 5, 2026, an attacker walked 135 BRO tokens into Solv Protocol's BitcoinReserveOffering vault and walked out with 567 million. A 4.2 million times inflation, delivered in a single transaction with 22 reentrant calls.
Those 567 million BRO were then swapped for 38 SolvBTC, worth roughly 2.7 million dollars at the time. The funds were laundered through RailGun within hours.
The vulnerability was reentrancy. The bug class has a name, a CVE family, a dedicated OpenZeppelin modifier, and an entire chapter in every smart contract security curriculum. It has been the textbook example of what to prevent since 2016.
The contract had no reentrancy guard. It was also not audited before deployment.
A protocol sitting on over a billion dollars of Bitcoin reserves shipped an unaudited vault with a classic reentrancy bug. This is a story about what happens when ERC-3525's design surface meets ordinary developer assumptions.
What Solv Protocol Is
Solv is one of the larger Bitcoin-denominated DeFi protocols. It issues SolvBTC, a wrapped representation of Bitcoin, along with structured yield products backed by Bitcoin collateral. The BRO token (Bitcoin Reserve Offering) was a claim on a pool of BTC-denominated yield.
The BitcoinReserveOffering vault accepted deposits of Solv's ERC-3525 position tokens and minted BRO in proportion.
This is where the design got interesting.
The ERC-3525 Wrinkle
ERC-3525 is a semi-fungible token standard. Each token has an ID like ERC-721, but also a "value" field and a "slot" that groups it with fungible counterparts. Two ERC-3525 tokens in the same slot can be split, merged, or partially transferred by value.
The important detail for this exploit: ERC-3525 inherits from ERC-721. Every ERC-3525 token is an ERC-721 token. Every ERC-3525 safe transfer invokes the ERC-721 onERC721Received callback on the receiving contract.
That callback runs inside the transfer flow. It runs before the transfer returns. And it can do whatever it wants.
If the receiving contract performs state updates after the transfer completes, the callback gets a window to operate on stale state. Classic checks-effects-interactions territory.
The Vulnerable Function
Here is the pattern in the vault's mint function, reconstructed from the on-chain bytecode and post-mortem analysis:
function mint(uint256 tokenId) external {
require(!processedTokens[tokenId], "already minted");
uint256 value = positionToken.balanceOf(tokenId);
// External call. Triggers onERC721Received on msg.sender.
positionToken.safeTransferFrom(msg.sender, address(this), tokenId);
// State update happens AFTER the external call.
processedTokens[tokenId] = true;
_mint(msg.sender, value * broPerUnit);
}
Three lines. One of the oldest patterns in the book.
- The check happens first.
processedTokens[tokenId]is still false, so the call proceeds. - The external call is
safeTransferFrom, which invokesonERC721Receivedon the attacker's contract. - Inside that callback, the attacker calls
mintagain with a differenttokenId. The check for that second token ID also passes, becauseprocessedTokenshas not been updated for it either. - By the time the outer call's state update finally runs, the attacker has already burned through 22 tokens in a single transaction.
Each recursive call minted BRO proportional to the token's underlying value. Each was counted as a legitimate deposit. At the end, the mapping was updated for all of them, but by then the BRO had already been minted, swapped, and moved.
The attacker deployed a custom contract that held a set of Solv position tokens and implemented onERC721Received to re-enter the vault. Twenty-two reentrant mint calls later, 135 BRO had been inflated to 567 million.
Why No Reentrancy Guard
The core question is not how the bug worked. The bug is a first-chapter reentrancy. The real question is how a production contract managing Bitcoin reserves shipped without nonReentrant on a function that performs an external call before a state update.
From what the post-mortem implies and what the bytecode shows, the developers appear to have assumed the external call was safe because safeTransferFrom is "just a transfer."
Two things went wrong with that assumption.
One. safeTransferFrom invokes the recipient's callback. The "safe" in the name refers to making sure the recipient can handle the token, not that the function call cannot cause side effects. Any function that ends up calling into untrusted code can be a reentrancy vector.
Two. The contract is both the ERC-3525 recipient and the state holder. The callback target is address(this). The callback calls into functions on the same contract. The call does not even look "external" at the conceptual level, but at the EVM level every call through a function signature goes through the call stack and can re-enter.
When you combine ERC-3525's mandatory callback semantics with a vault that updates state after the deposit and does not use a reentrancy guard, the vulnerability is mechanical. No novel chain of logic is needed. No price manipulation. Just a properly formatted contract as the sender.
What Odin Scan Would Have Flagged
This is a case where an automated scan catches the bug trivially, on any meaningful setup.
The pattern we detect is straightforward:
- A function performs a state-changing external call.
- A state update occurs after that external call in the same function.
- The function lacks
nonReentrantor equivalent guard.
Three conditions. All three present here. The finding category is Reentrancy, severity Critical, with the classic Checks-Effects-Interactions reasoning.
For ERC-3525 specifically, we layer a heuristic that recognizes safeTransferFrom patterns on tokens that implement callbacks. This elevates the severity because ERC-3525's design makes the callback path more implicit than in plain ERC-721 use cases.
A scan of the vault in CI/CD would have returned:
Critical. Reentrancy. BitcoinReserveOffering.sol line [X]. The function mint performs safeTransferFrom before updating processedTokens. ERC-3525 invokes onERC721Received during safeTransferFrom, which an attacker-controlled receiver can use to re-enter mint with a different tokenId before state is updated. Apply nonReentrant modifier or follow checks-effects-interactions by moving processedTokens[tokenId] = true before the transfer.
Free. In under a minute. Before deployment.
The Audit Gap
Solv has had audits. The core SolvBTC contracts, the vault registry, the major deposit/withdraw flows. What did not get audited was the specific BitcoinReserveOffering vault.
This is a recurring pattern in DeFi. A protocol team pays for audits at launch. Subsequent products, extensions, and new vaults are deployed between audits or after audits without re-entering the audit queue. The cost of a full audit for every new vault is prohibitive, so it gets skipped.
The decision to skip is usually rational in isolation. The vault is small. It launches with a cap. It will be monitored. The team has internal review.
Then it gets exploited.
An audit catches what a reentrancy guard catches, but it takes weeks and costs five figures. Automated scanning catches the same class of issues in seconds and runs on every PR. Audits are for defense in depth, novel logic, and economic reasoning. Automated scanning is for making sure the baseline is not broken before the auditors even start.
Both layers together get you a lot further than either alone. A protocol that ships an unaudited contract should, at minimum, have every commit running through a CI/CD scanner that would catch the top 10 vulnerability classes.
What Solv Did Next
Solv offered a 10 percent bounty to the attacker for returning funds. At publication time, there has been no public confirmation that the bounty was accepted.
Solv also paused the affected vault and announced a post-mortem commitment. The team has stated plans to route all future vault deployments through a pre-deployment audit queue.
These are the right responses. They are also the responses any protocol gives after an incident. The pattern is that these commitments weaken over time as pressure to ship returns.
What You Should Do This Week
If you are running a DeFi protocol with a vault architecture:
Grep your repo for external calls in functions that update state afterward. This is the most common reentrancy shape. If you find any, apply nonReentrant or reorder the function to update state first. Every one of them. Not just the ones that look exploitable.
Verify every deposit entry point has a reentrancy guard. deposit, mint, withdraw, redeem. Any function where tokens move between user and contract. The cost of the modifier is a small amount of gas. The cost of not having it is illustrated by this article.
If you use ERC-3525 or any callback-invoking token standard, treat every transfer as an external call to untrusted code. Because it is. The safe in safeTransferFrom does not mean "safe from reentrancy."
Run automated scanning on every PR, not just at audit time. Reentrancy detection is one of the most mature capabilities in the entire static analysis space. There is no excuse for a production contract to ship with an obvious reentrancy vector in 2026.
The Takeaway
Reentrancy has been a known attack class for ten years. The fix is documented. The modifier is in every major security library. And yet, in 2026, a protocol with a billion dollars of Bitcoin reserves shipped an unaudited vault that got drained by it.
The bug did not require sophistication. The exploit contract fit on a single screen. The only reason it worked is that the door was unlocked, and nobody checked before opening it.
Odin Scan would have checked. So would slither, so would any decent static analyzer that knows what the words checks-effects-interactions mean. The question is not whether tools exist. The question is whether teams wire them into their deployment pipeline and honor what they find.
Running a DeFi protocol? Start a free Odin Scan trial and have your repo scanned in under five minutes. Every PR. Every vault. Every deployment script. The baseline should be boring.
Sources: Halborn post-mortem, Nomos Labs analysis, Solv Protocol announcement, Invezz coverage.