TL;DR: On March 7, 2026, an attacker exploited a reentrancy vulnerability in Solv Protocol's Bitcoin Reserve Offering (BRO) vault. The attack leveraged the fact that ERC-3525 tokens inherit ERC-721's mandatory onERC721Received callback — creating an unexpected re-entry point during doSafeTransferIn. A 135-token deposit was inflated to 567 million tokens, swapped for ~38 SolvBTC (~$2.7M), and drained from the vault. This wasn't a novel vulnerability class — it was a well-known pattern hiding in a token standard inheritance chain.
The Protocol: Solv and SolvBTC
Solv Protocol is a Bitcoin reserve protocol that lets users exchange BTC for SolvBTC — a 1:1 pegged wrapper that enables Bitcoin holders to participate in DeFi across multiple chains (staking, lending, borrowing). The Bitcoin Reserve Offering (BRO) system is the mechanism through which users deposit tokens to mint SolvBTC-backed positions.
The BRO vault uses ERC-3525 tokens — a semi-fungible token standard that combines ERC-721's non-fungibility with fungible value slots. This is where the story gets interesting.
The Token Standard Trap: ERC-3525 ⊃ ERC-721
Here's the critical design decision that made this exploit possible:
ERC-3525 is built on top of ERC-721. This isn't just an implementation detail — it's baked into the standard. Every ERC-3525 token IS an ERC-721 token, which means every safe transfer of an ERC-3525 token must call onERC721Received on the receiving contract.
ERC-3525 Token
├── Semi-fungible value slots (new)
├── Metadata (inherited from ERC-721)
└── Safe transfer callbacks (inherited from ERC-721)
└── onERC721Received() ← THIS IS THE PROBLEM
Most developers think about reentrancy in terms of ETH transfers or explicit callback patterns. But token standard inheritance creates implicit callbacks that are easy to overlook — especially when the receiving contract was designed for ERC-3525 semantics but inherits ERC-721 behavior.
The Attack Flow
Here's exactly what happened, step by step:
Step 1: The Deposit
The attacker calls the BRO vault's deposit function with an ERC-3525 token (135 BRO tokens).
Step 2: doSafeTransferIn Triggers
The vault's internal doSafeTransferIn function initiates a safe transfer to ingest the ERC-3525 token. This function follows the standard ERC-721 safe transfer pattern:
doSafeTransferIn(token)
├── Transfer token to vault
├── Call onERC721Received() on vault ← CALLBACK FIRES HERE
│ └── Attacker re-enters: triggers second mint
│ └── Second mint COMPLETES FIRST
└── First mint completes (with inflated state)
Step 3: The Reentrancy
When doSafeTransferIn calls onERC721Received on the vault contract, the attacker's malicious contract intercepts this callback and re-enters the minting function. Because the first mint hasn't finalized its state updates yet, the second mint operates on stale state — effectively minting tokens twice for a single deposit.
Step 4: The Inflation
The attacker repeated this 22 times. Each iteration compounded the double-minting effect:
| Iteration | Input | Output |
|---|---|---|
| 1 | 135 BRO | ~270 BRO (2x) |
| 2-5 | Compounding | Exponential growth |
| ... | ... | ... |
| 22 | — | 567,000,000 BRO |
Step 5: The Drain
The 567 million inflated BRO tokens were swapped for approximately 38.0474 SolvBTC through the vault's redemption mechanism. At the 1:1 BTC peg, this was worth approximately $2.7 million.
Why Traditional Reentrancy Guards Failed
The standard defense against reentrancy is the Checks-Effects-Interactions (CEI) pattern:
// CEI Pattern
function deposit(uint amount) external {
require(amount > 0); // CHECK
balances[msg.sender] += amount; // EFFECT
token.safeTransferFrom(...); // INTERACTION
}
But the Solv vault's architecture made CEI insufficient:
The interaction IS the effect. The
doSafeTransferIncall both transfers the token AND triggers the callback — you can't separate the interaction from the state change when the token standard mandates a callback during transfer.Cross-function reentrancy. The callback doesn't re-enter the same function — it triggers a separate mint pathway. Single-function reentrancy guards (
nonReentrantmodifiers) might not cover this if they're applied per-function rather than per-contract.Inherited behavior is invisible. Developers who read the ERC-3525 spec might not realize that every safe transfer inherits ERC-721's callback requirement. The reentrancy surface is in the parent standard, not the one you're implementing.
The Deeper Lesson: Token Standard Composition Is an Attack Surface
This hack reveals a systemic risk in how token standards compose:
ERC-20: No callbacks → Low reentrancy risk
ERC-721: onERC721Received → Known reentrancy surface
ERC-1155: onERC1155Received → Known reentrancy surface
ERC-3525: Inherits ERC-721 → INHERITED reentrancy surface
ERC-4626: Wraps ERC-20 → Lower risk, but share inflation possible
Future standards: ??? → Inherited surfaces compound
Every new token standard that extends an existing one inherits its attack surfaces. And those surfaces might interact with the new standard's semantics in unexpected ways.
What Makes This Class Particularly Dangerous
Auditors focus on the implemented standard. If you're auditing an ERC-3525 vault, you're reading the ERC-3525 spec. The reentrancy surface lives in the ERC-721 spec.
Automated tools may miss it. Static analyzers that check for reentrancy might not trace through token standard inheritance hierarchies to identify callback surfaces.
The "safe" in
safeTransferis misleading. The callback exists to make transfers "safe" for receiving contracts — but it creates an attack surface that makes the sending contract unsafe.
Defense Patterns
For Protocol Developers
1. Contract-wide reentrancy guards
Don't use per-function nonReentrant. Use a single global lock:
uint256 private _locked = 1;
modifier globalNonReentrant() {
require(_locked == 1, "REENTRANCY");
_locked = 2;
_;
_locked = 1;
}
Apply this to ALL state-changing functions, not just the one handling the transfer.
2. Map your token standard inheritance tree
Before integrating any token standard, trace its full inheritance chain:
Your Integration
└── Uses ERC-3525
└── Extends ERC-721
└── Mandates onERC721Received callback
└── REENTRANCY SURFACE: Document and guard
3. Prefer pull-over-push for minting
Instead of minting during the deposit transaction:
function deposit(token) external globalNonReentrant {
// Transfer token in
// Record pending claim (EFFECT before INTERACTION)
pendingClaims[msg.sender] += amount;
}
function claimMint() external globalNonReentrant {
uint amount = pendingClaims[msg.sender];
pendingClaims[msg.sender] = 0;
_mint(msg.sender, amount);
}
4. Use transfer hooks carefully with ERC-3525
If you must use safeTransferFrom, ensure all state updates are finalized before the transfer:
// Update ALL state BEFORE the transfer
totalMinted += amount;
userBalance[msg.sender] += amount;
// THEN do the transfer (which triggers callback)
token.safeTransferFrom(from, address(this), tokenId);
// Verify state hasn't been corrupted
require(totalMinted == expectedTotal, "STATE_CORRUPTED");
For Auditors
- Always trace token standard hierarchies. If a contract interacts with any token that extends ERC-721 or ERC-1155, assume a callback surface exists.
- Test cross-function reentrancy specifically. Write exploit contracts that re-enter through different functions via the callback.
- Check for "hidden" ERC-721 compliance. ERC-3525, ERC-4907, and other standards that extend ERC-721 all inherit its callback requirement.
Impact and Response
- Loss: ~$2.7M (38.0474 SolvBTC)
- Affected users: Fewer than 10 (single vault)
- Response: Solv Protocol pledged full compensation, offered 10% white-hat bounty
- Post-incident: Appointed Fuzzland as runtime Risk Guardian for 24/7 monitoring
- Collaborated with: Hypernative Labs, SlowMist, CertiK
Conclusion
The Solv Protocol hack is a textbook example of how well-understood vulnerabilities (reentrancy) can hide in unexpected places (token standard inheritance). The ERC-3525 standard is well-designed — but its ERC-721 foundation carries decades-old attack surfaces that don't disappear just because a new standard is layered on top.
For builders: map your inheritance surfaces before writing a single line of business logic. For auditors: the attack surface isn't just what's implemented — it's everything that's inherited.
The $2.7M wasn't lost to a novel zero-day. It was lost to a pattern we've known about since 2016, wearing a new hat.
Top comments (0)