DEV Community

ohmygod
ohmygod

Posted on

Anatomy of the Solv Protocol Hack: How ERC-3525 Reentrancy Drained $2.7M

The Incident

On March 2026, Solv Protocol — a Bitcoin reserve protocol that lets users wrap BTC into SolvBTC for cross-chain DeFi — lost approximately $2.7 million through a smart contract exploit targeting its BitcoinReserveOffering (BRO) vault.

The attacker didn't need a flashloan. They didn't exploit an oracle. They found something more subtle: a cross-standard reentrancy hidden in the interaction between ERC-3525 and ERC-721.

Understanding the Attack Vector

ERC-3525: The Semi-Fungible Token Standard

ERC-3525 defines semi-fungible tokens (SFTs) — tokens that have both an ID (like NFTs) and a value (like ERC-20). The critical design decision: ERC-3525 inherits from ERC-721.

This means every ERC-3525 token is also an ERC-721 token. And ERC-721 mandates calling onERC721Received during safe transfers.

The Double-Mint Flaw

Here's the vulnerable flow in the BRO vault:

1. User deposits ERC-3525 token via doSafeTransferIn()
2. doSafeTransferIn() triggers token transfer
3. Transfer triggers onERC721Received() callback ← REENTRANCY POINT
4. Callback mints BRO tokens (Mint #2 — completes first)
5. Original deposit handler mints BRO tokens (Mint #1 — completes second)
Enter fullscreen mode Exit fullscreen mode

The vault's doSafeTransferIn ingests the ERC-3525 token and triggers a mint. But because of the ERC-721 inheritance, the safe transfer also calls onERC721Received on the receiver — which triggers a second mint. Since mint #2 completes before mint #1, the contract's state hasn't been updated to reflect the first operation.

This is classic reentrancy, but hidden behind cross-standard inheritance.

The Exploit in Practice

The attacker:

  1. Triggered the double-mint vulnerability 22 times
  2. Converted 135 BRO into ~567 million BRO tokens
  3. Swapped the inflated BRO for ~38 SolvBTC
  4. Walked away with ~$2.7M (SolvBTC trades 1:1 with BTC)

Why Traditional Audits Missed This

CEI Isn't Enough

The Check-Effects-Interactions (CEI) pattern is the standard reentrancy defense. But in this case:

  • The check was correct — the vault verified the deposit
  • The effects were applied — state was updated after the mint
  • The interaction was the problem — but it was hidden inside the token standard itself

The reentrancy didn't come from an external call to an untrusted contract. It came from a mandatory callback defined by the token standard. The vault was following best practices for handling ERC-3525 tokens — it just didn't account for the ERC-721 callback that comes along for the ride.

Cross-Standard Inheritance is a Blind Spot

Most auditors check reentrancy within a single standard. But when Token Standard A inherits from Token Standard B, you get two sets of callbacks, two sets of hooks, and two sets of assumptions that can conflict.

The ERC-3525 → ERC-721 inheritance means:

  • safeTransferFrom triggers ERC-3525's value transfer logic AND ERC-721's onERC721Received
  • If both paths can trigger state changes, you have a reentrancy window

Detection Patterns

What to Look For in Audits

1. Token standard inheritance chains

ERC-3525 → ERC-721 → ERC-165
ERC-4626 → ERC-20
ERC-1155 → (multi-token callbacks)
Enter fullscreen mode Exit fullscreen mode

Any time a token standard inherits from another, check if both levels have callbacks or hooks.

2. Safe transfer functions with state changes

// RED FLAG: State change after safe transfer
function deposit(uint256 tokenId) external {
    token.safeTransferFrom(msg.sender, address(this), tokenId);
    // ↑ This triggers onERC721Received before reaching ↓
    _mint(msg.sender, calculateAmount(tokenId));
}
Enter fullscreen mode Exit fullscreen mode

3. Multiple mint/burn paths in a single transaction

If a function can trigger minting through more than one code path, verify each path is aware of the others.

Foundry Test Pattern

contract ReentrancyTest is Test {
    BROVault vault;
    MaliciousReceiver attacker;

    function testDoubleMinReentrancy() public {
        attacker = new MaliciousReceiver(address(vault));
        uint256 tokenId = erc3525.mint(address(attacker), 1, 100);

        uint256 balBefore = bro.balanceOf(address(attacker));
        attacker.attack(tokenId);
        uint256 balAfter = bro.balanceOf(address(attacker));

        assertEq(balAfter - balBefore, expectedSingleMint);
    }
}

contract MaliciousReceiver is IERC721Receiver {
    BROVault vault;
    bool attacked;

    function onERC721Received(
        address, address, uint256, bytes calldata
    ) external returns (bytes4) {
        if (!attacked) {
            attacked = true;
            vault.deposit(savedTokenId);
        }
        return this.onERC721Received.selector;
    }
}
Enter fullscreen mode Exit fullscreen mode

Mitigation Strategies

For Protocol Developers

1. Use reentrancy guards on all state-changing functions

function deposit(uint256 tokenId) external nonReentrant {
    token.safeTransferFrom(msg.sender, address(this), tokenId);
    _mint(msg.sender, calculateAmount(tokenId));
}
Enter fullscreen mode Exit fullscreen mode

2. Apply CEI at the cross-standard level

Update state before any transfer that could trigger callbacks:

function deposit(uint256 tokenId) external nonReentrant {
    uint256 amount = calculateAmount(tokenId);
    _mint(msg.sender, amount);  // Effects first
    token.safeTransferFrom(msg.sender, address(this), tokenId);  // Interaction last
}
Enter fullscreen mode Exit fullscreen mode

3. Audit all token standards for inherited callbacks

If you're building a vault that accepts ERC-3525, you must also handle:

  • onERC721Received (from ERC-721 inheritance)
  • supportsInterface for both ERC-3525 and ERC-721
  • Potential onERC3525Received (if defined by the implementation)

For Auditors

Add this to your checklist:

  • [ ] Map all token standard inheritance chains in the codebase
  • [ ] Identify every callback hook in each standard layer
  • [ ] Test reentrancy through each callback independently
  • [ ] Verify nonReentrant guards cover cross-standard paths
  • [ ] Check if state mutations happen between transfer initiation and callback completion

The Bigger Picture

The Solv hack is a case study in composability risk. DeFi's power comes from composing standards and protocols. But each layer of composition adds potential interaction paths that may not be obvious:

Risk Layer Example
Single-contract reentrancy Classic CEI violation
Cross-function reentrancy Read-only reentrancy via view functions
Cross-contract reentrancy Callback to another protocol
Cross-standard reentrancy ERC-3525 → ERC-721 callback (Solv)
Cross-chain reentrancy Bridge message replay

As DeFi matures and more complex token standards emerge (ERC-3525, ERC-4337, ERC-6551), we'll see more of these cross-standard vulnerabilities. The attack surface isn't just in the code you write — it's in the standards you inherit.


DreamWork Security specializes in smart contract auditing across Solana and EVM ecosystems. Follow for weekly security research and vulnerability analysis.

Top comments (0)