Full Report
Solv Protocol is a wrapped Bitcoin implementation. The standard token implementation is ERC-20; their token is ERC-3525, which is a semi-fungible token standard. It has ERC-721 token IDs but balances that are fungible. Instead of thinking about this as balances, positions are more accurate. They are supposed to behave like bond-like claims, tranche positions, and other financial positions. With this standard, there are two types of transfers: whole token transfers (similar to ERC-721) and value transfers (similar to a part of a value). Depending on the function being used, the transferred value can be merged into another token ID or transfer the whole value to a recipient. So, the contract has to implement both the IERC721Receiver and IERC3525Receiver. The BitcoinReserveOffering contract takes an ERC-3525 position, or some value from that position, and wraps it into an ERC20-like token. So, the underlying asset is a semi-fungible position, and the wrapped turns the deposited position value into fungible shares. When burning the shares later, the position value is given back. The wrapper keeps track of an internally held token ID that acts the contract's main pooled position. If the contract receives value, the deposits are merged into this token ID. The vulnerability appears to stem from a set of complexity in supporting both ERC-3525 and ERC-721 style transfers. Both of these must have callbacks. If a user deposits their entire SFT balance, mint() will call the ERC3525TransferHelper.doSafeTransferIn(). Eventually, this will trigger a onERC721Received() callback that calls _mint(). Once control returns, mint() is called again. So, this leads to a double mint by design. The contract also supports depositing on part of the SFT value. Upon doing this transfer, onERC3525Received() gets called on the contract after triggering a transfer via transferFrom(). The callback contains a _mint() then the control flow executes another mint(). This leads to another double-mint path. To exploit the vulnerability, just go back via calling burn(). The attack is calling mint on a position, receive double the ERC20 shares, call burn to redeem the inflated shares back into SFT value and then redo the process again to profit more. It's crazy how fundamental this vulnerability is to the protocol... I imagine that the math for tokens to share was hard to reason about and it was just assumed to be working as intended, when it really wasn't. Overall, a fantastic write up on the vulnerability!
Analysis Summary
# Incident Report: Solv Protocol Callback-Driven Double Minting
## Executive Summary
On March 6, 2026, Solv Protocol's `BitcoinReserveOffering` contract was exploited for approximately $2.7M. The vulnerability involved a fundamental accounting error where deposits were processed twice due to an unintended interaction between ERC-3525/ERC-721 receiver callbacks and the primary minting function. An attacker was able to inflate their share balance through this "double-minting" loop and redeem the excess shares for underlying assets.
## Incident Details
- **Discovery Date:** March 6, 2026
- **Incident Date:** March 6, 2026
- **Affected Organization:** Solv Protocol
- **Sector:** Decentralized Finance (DeFi) / Bitcoin Liquid Staking
- **Geography:** Global / Distributed
## Timeline of Events
### Initial Access
- **Date/Time:** March 6, 2026
- **Vector:** Smart Contract Exploitation (Logic Vulnerability)
- **Details:** The attacker targeted the `BitcoinReserveOffering` contract, which acts as a wrapper for ERC-3525 semi-fungible tokens (SFTs), converting them into fungible ERC-20 shares.
### Lateral Movement
- **Details:** The attacker did not move through a traditional network but exploited the protocol’s internal accounting logic. By calling the `mint()` function with an SFT position, they triggered a series of callbacks (`onERC721Received` or `onERC3525Received`).
### Data Exfiltration/Impact
- **Details:** The exploit allowed the attacker to receive double the intended amount of wrapped ERC-20 shares for a single deposit. The attacker then called the `burn()` function to redeem these inflated shares for the underlying SFT value, effectively draining the protocol's reserves.
### Detection & Response
- **Detection:** Detected via on-chain monitoring post-exploit (Transaction Hash: `0x44e637c7d85190d376a52d89ca75f2d208089bb02b7c4708ad2aaae3a97a958d`).
- **Response:** Analysis conducted by security firms (e.g., BlockSec and Taichi Audit) to identify the root cause in the `BitcoinReserveOffering` contract.
## Attack Methodology
- **Initial Access:** Exploitation of the `mint()` function on the `BitcoinReserveOffering` contract.
- **Persistence:** Not applicable; the attack was a discrete set of on-chain transactions.
- **Privilege Escalation:** Not applicable; the vulnerability was accessible to any user.
- **Defense Evasion:** Use of complex semi-fungible token (SFT) interactions to mask the accounting double-dip.
- **Discovery:** Identifying that both the receiver callback and the main function body contained `_mint()` instructions.
- **Impact:** Draining of approximately $2.7M in assets by repeating a mint-burn cycle.
## Impact Assessment
- **Financial:** Lost approximately $2.7 Million USD.
- **Data Breach:** None (No PII or off-chain data compromised).
- **Operational:** Disruption of the Bitcoin Reserve Offering and subsequent need for contract upgrades/redesign.
- **Reputational:** High; the vulnerability was noted as a "fundamental" accounting failure in a standard protocol feature.
## Indicators of Compromise
- **Transaction:** `0x44e637c7d85190d376a52d89ca75f2d208089bb02b7c4708ad2aaae3a97a958d`
- **Contract Address (Vulnerable):** `0x15f7c1ac69f0c102e4f390e45306bd917f21cfcf`
- **Behavioral:** Rapid minting of wrapped shares followed by immediate burning for higher underlying value than deposited.
## Response Actions
- **Containment:** Community and security researcher alert regarding the vulnerable contract.
- **Eradication:** Identification of the flawed `mint()` logic where `_mint()` was called twice (once in the callback and once in the function body).
- **Recovery:** Analysis of the $2.7M loss for potential recovery or protocol compensation.
## Lessons Learned
- **Callback Risk:** Receiver callbacks (common in ERC-721 and ERC-3525) can high-jack execution flow. If a callback performs a state change (like minting), the parent function must not repeat that state change.
- **State Management:** Invariants must be checked across the entire execution flow, including nested calls triggered by transfers.
- **Complexity Danger:** Supporting multiple token standards (ERC-20, ERC-721, and ERC-3525) simultaneously increases the attack surface for accounting errors.
## Recommendations
- **Reentrancy Guards:** Implement `nonReentrant` modifiers on functions that handle transfers and minting.
- **Invariant Testing:** Implement formal verification or invariant testing to ensure that `share_supply` always matches `deposited_value`.
- **Logic Consolidation:** Ensure that `_mint()` is called in exactly one place within a transaction flow, regardless of which receiver callback is triggered.