TL;DR
On March 5, 2026, an attacker exploited a self-reentrancy vulnerability in Solv Protocol's vault contract, stealing $2.73 million worth of SolvBTC in a single transaction. The root cause? An onERC721Received callback that minted tokens before the calling function finished its own mint — a textbook self-reentrancy. Here's what happened, why it matters even if you're not a Solidity developer, and how you can avoid this class of bugs.
Background: What Happened?
Solv Protocol is a DeFi platform that issues SolvBTC. On March 5, 2026, an attacker executed a single transaction that:
- Started with 135 BRO tokens
- Looped through the mint function 22 times, inflating 135 tokens into 567 million BRO
- Swapped stolen BRO → SolvBTC → WBTC → WETH → ETH through Uniswap V3
- Walked away with 1,211 ETH (~$2.73M) The vulnerable contract is 0x014e6F6ba7a9f4C9a51a0Aa3189B5c0a21006869 and the attack transaction can be viewed here. Here's the kicker: five audit firms had reviewed parts of Solv Protocol's codebase — but none of them covered this particular contract. It was unaudited. A Quick Primer on Key Concepts ERC-721 (NFTs) ERC-721 is the Ethereum standard for non-fungible tokens — each token has a unique ID. Think of them as unique digital items. safeTransferFrom This is a "safe" transfer function defined by ERC-721. When you call safeTransferFrom, the contract checks if the receiving address is a smart contract. If it is, the transfer function calls back into the receiver via onERC721Received. ERC-3525 (Semi-Fungible Tokens) ERC-3525 is a newer standard for semi-fungible tokens — tokens that have both an ID (like an NFT) and a balance (like an ERC-20 token). It inherits from ERC-721, which means every ERC-3525 token also implements the ERC-721 interface. The Vulnerability: Self-Reentrancy function mint(address to, uint256 collateralAmount, uint256 nftId) external { collateralToken.safeTransferFrom(msg.sender, address(this), collateralAmount); nftToken.safeTransferFrom(msg.sender, address(this), nftId); // triggers callback! _mint(to, collateralAmount); // 2nd mint }
function onERC721Received(address, address from, uint256 tokenId, bytes calldata data)
external returns (bytes4) {
uint256 amount = _parseCollateralAmount(data);
_mint(from, amount); // 1st mint IN CALLBACK
return this.onERC721Received.selector;
}
The Attack Flow
- Attacker calls mint()
- safeTransferFrom() is called... → Detects receiving contract → Calls onERC721Received() on the same contract → _mint() runs (1st mint) → Callback returns
- Back in mint(), _mint() runs again (2nd mint)
Result: Two mints for one mint() call.
Self-reentrancy is more dangerous than classic reentrancy because:
- It's easier to overlook — one contract, not interactions between two
- The vulnerable code "looks correct" at a glance The Exploit: Turning $0 into $2.73M The attacker looped the double-mint 22 times in a single transaction. 135 BRO → 567 million BRO → 38.0474 SolvBTC → 1,211 ETH. How to Fix It Fix 1: Reentrancy Guard contract SolvVault is ReentrancyGuard { function mint(...) external nonReentrant { collateralToken.safeTransferFrom(msg.sender, address(this), collateralAmount); nftToken.safeTransferFrom(msg.sender, address(this), nftId); _mint(to, collateralAmount); } }
Fix 2: CEI Pattern
function mint(...) external {
_mint(to, collateralAmount); // Effects FIRST
collateralToken.safeTransferFrom(...); // Interactions LAST
nftToken.safeTransferFrom(...);
}
Fix 3: Empty Callback
function onERC721Received(...) external pure returns (bytes4) {
return this.onERC721Received.selector;
}
Key Takeaways
Lesson
Detail
Self-reentrancy is reentrancy
Same attack vector, harder to spot
Callback functions are dangerous
All receiver hooks are potential reentrancy entry points
CEI pattern saves lives
Always update state before external calls
Use ReentrancyGuard
One-line fix, prevents million-dollar bugs
Audits aren't enough
Coverage matters more than the number of audits
References
- Attack TX on Etherscan
- ERC-3525 Specification
- ERC-721 Specification
- OpenZeppelin ReentrancyGuard
Top comments (0)