DEV Community

ohmygod
ohmygod

Posted on

The Gondi NFT Lending Exploit: How a Missing Ownership Check Let Attackers Drain 78 NFTs Worth $230K

Intro

On March 8–10, 2026, attackers exploited a logical flaw in Gondi's "Sell & Repay" smart contract to drain 78 NFTs — including 44 Art Blocks, 10 Doodles, and 2 Beeple pieces — worth approximately $230,000. The root cause? A newly deployed "Purchase Bundler" function that failed to verify whether the caller actually owned (or had borrowed against) the NFTs being withdrawn.

This wasn't a flash loan attack. It wasn't an oracle manipulation. It was the most basic vulnerability class in smart contract security: missing access control.

How Gondi's Sell & Repay Works

Gondi is an NFT lending protocol. Users deposit NFTs as collateral, receive loans, and can later repay or sell. The "Sell & Repay" feature lets borrowers sell collateralized NFTs directly, with loan repayment automatically deducted from the sale proceeds.

The flow looks like:

1. Borrower lists collateralized NFT for sale
2. Buyer purchases through Purchase Bundler
3. Contract automatically repays the loan from sale proceeds
4. NFT transfers to buyer, remaining funds go to borrower
Enter fullscreen mode Exit fullscreen mode

On February 20, 2026, Gondi deployed a new version of this contract with an updated Purchase Bundler function. This update introduced the vulnerability.

The Vulnerability: No Ownership Verification

The critical flaw was in the Purchase Bundler's withdrawal logic. When processing an NFT withdrawal, the contract checked whether the NFT existed in Gondi's escrow — but never verified that the caller was the legitimate owner or active borrower.

Here's a simplified reconstruction of the vulnerable pattern:

// VULNERABLE — DO NOT USE
contract PurchaseBundler {
    mapping(uint256 => address) public escrowedNFTs;

    function withdrawFromEscrow(
        address nftContract,
        uint256 tokenId
    ) external {
        // ❌ Only checks if NFT is in escrow
        // ❌ Never checks if msg.sender is the depositor or borrower
        require(
            escrowedNFTs[tokenId] != address(0),
            "Not in escrow"
        );

        // Transfers NFT to caller — anyone can call this!
        IERC721(nftContract).transferFrom(
            address(this),
            msg.sender,
            tokenId
        );

        delete escrowedNFTs[tokenId];
    }
}
Enter fullscreen mode Exit fullscreen mode

The fix is straightforward — add ownership verification:

// FIXED — Proper access control
contract PurchaseBundlerFixed {
    mapping(uint256 => address) public escrowedNFTs;
    mapping(uint256 => address) public nftDepositor;

    function withdrawFromEscrow(
        address nftContract,
        uint256 tokenId
    ) external {
        require(
            escrowedNFTs[tokenId] != address(0),
            "Not in escrow"
        );

        // ✅ Verify caller is the original depositor or active borrower
        require(
            msg.sender == nftDepositor[tokenId] ||
            _isActiveBorrower(msg.sender, tokenId),
            "Not authorized"
        );

        IERC721(nftContract).transferFrom(
            address(this),
            msg.sender,
            tokenId
        );

        delete escrowedNFTs[tokenId];
        delete nftDepositor[tokenId];
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Matters: The NFT Lending Attack Surface

Gondi's exploit exposes a pattern that's becoming increasingly common as NFT lending protocols grow. When NFTs sit in escrow contracts, any function that can move them becomes a critical attack surface. The Sell & Repay feature added a new code path for NFT movement — and that new path wasn't secured.

The Broader Pattern: Bundler Functions as Attack Vectors

"Bundler" or "multicall" patterns that batch multiple operations are particularly dangerous because they:

  1. Combine permissions — A bundler that can both withdraw from escrow and transfer NFTs has god-mode access
  2. Obscure ownership flows — Complex bundled operations make it hard to track who should authorize what
  3. Are deployed as upgrades — New bundler contracts often get escrow permissions without re-auditing the entire authorization model
// DANGEROUS PATTERN: Bundler with broad permissions
contract NaiveBundler {
    IEscrow public escrow;

    // This function can move ANY escrowed NFT
    // because the bundler contract itself has escrow permissions
    function bundle(bytes[] calldata actions) external {
        for (uint i = 0; i < actions.length; i++) {
            // Executes arbitrary actions with bundler's permissions
            // msg.sender context is LOST inside the escrow call
            (bool success,) = address(escrow).call(actions[i]);
            require(success);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The fix: implement per-action authorization within bundlers, not just at the contract level:

// SAFER: Per-action authorization
contract SecureBundler {
    IEscrow public escrow;

    function bundleWithdraw(
        address nftContract,
        uint256[] calldata tokenIds
    ) external {
        for (uint i = 0; i < tokenIds.length; i++) {
            // ✅ Verify ownership for EACH token
            require(
                escrow.getDepositor(tokenIds[i]) == msg.sender,
                "Not owner"
            );
            escrow.withdraw(nftContract, tokenIds[i], msg.sender);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The Attack Timeline

Time Event
Feb 20, 2026 New Purchase Bundler contract deployed
~17 days Vulnerability sits undetected
Mar 8, 2026 Attacker begins exploiting — first NFT drained
Mar 8-10 40 transactions drain 78 NFTs
Mar 10 Gondi disables Sell & Repay feature
Mar 10 Users advised to revoke approvals for 0xc104... contracts
Post-exploit Blockaid + independent auditor review remaining functions

Key observation: the vulnerability existed for 17 days before exploitation. This is a textbook window where automated monitoring could have caught suspicious withdrawal patterns.

Detection: What Would Have Caught This

1. Invariant Testing During Development

// Foundry invariant test
contract GondiInvariantTest is Test {
    PurchaseBundler bundler;

    function invariant_onlyOwnerCanWithdraw() public {
        // For every escrowed NFT, the only address that 
        // successfully withdrew it should be the depositor
        // or an active borrower
        uint256[] memory withdrawnTokens = bundler.getRecentWithdrawals();
        for (uint i = 0; i < withdrawnTokens.length; i++) {
            address withdrawer = bundler.lastWithdrawer(withdrawnTokens[i]);
            address depositor = bundler.originalDepositor(withdrawnTokens[i]);
            assertTrue(
                withdrawer == depositor || 
                bundler.wasActiveBorrower(withdrawer, withdrawnTokens[i]),
                "Unauthorized withdrawal detected"
            );
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

2. On-Chain Monitoring

# Monitor for suspicious NFT withdrawals from Gondi escrow
from web3 import Web3

def monitor_gondi_withdrawals(w3, escrow_address):
    transfer_topic = w3.keccak(text="Transfer(address,address,uint256)")

    def check_block(block_num):
        logs = w3.eth.get_logs({
            'fromBlock': block_num,
            'toBlock': block_num,
            'address': escrow_address,
            'topics': [transfer_topic.hex()]
        })

        for log in logs:
            from_addr = '0x' + log['topics'][1].hex()[-40:]
            to_addr = '0x' + log['topics'][2].hex()[-40:]
            token_id = int(log['topics'][3].hex(), 16)

            # Alert if NFTs leaving escrow to non-depositor
            if from_addr.lower() == escrow_address.lower():
                depositor = get_original_depositor(token_id)
                if to_addr.lower() != depositor.lower():
                    alert(f"⚠️ NFT #{token_id} withdrawn by "
                          f"non-depositor {to_addr}")

    return check_block
Enter fullscreen mode Exit fullscreen mode

3. Pre-Deployment Checklist for Bundler Contracts

Every bundler or multicall contract that touches escrowed assets should pass this checklist before deployment:

  • [ ] Ownership verification: Does every withdrawal path verify msg.sender is the depositor or authorized borrower?
  • [ ] Permission isolation: Does the bundler maintain caller context when interacting with escrow?
  • [ ] Approval scope: Are approvals granted to the bundler limited to specific operations?
  • [ ] Upgrade review: If this replaces an existing contract, does it maintain all access control invariants?
  • [ ] Integration test: Are there tests where a non-owner attempts every withdrawal path?

Lessons for NFT Protocol Developers

1. Treat Every New Code Path as a Potential Bypass

Gondi's core escrow was secure. The vulnerability was in a new function that bypassed the core's access control. When adding features like "Sell & Repay," audit them not just for their own logic but for how they interact with existing security boundaries.

2. Bundler Contracts Need Extra Scrutiny

Bundler/multicall patterns are convenience features that aggregate permissions. They're inherently dangerous because they execute with the bundler's permissions, not the caller's. Every action inside a bundler must independently verify authorization.

3. NFTs in Escrow = High-Value Targets

Unlike fungible tokens where you might lose a percentage, NFT escrow exploits are all-or-nothing per asset. A single missing check can drain irreplaceable art. The security bar for NFT escrow operations should be higher than for ERC-20 vaults.

4. Time-to-Detection Matters

17 days between deployment and exploitation is an eternity. Automated invariant monitoring (tracking that only depositors withdraw their own NFTs) would have caught this on the first malicious transaction.

Conclusion

The Gondi exploit is a humbling reminder that the most devastating vulnerabilities are often the simplest. No flash loans, no oracle manipulation, no complex cross-protocol interactions — just a missing require statement that let anyone withdraw anyone else's NFTs.

As NFT lending protocols proliferate (Gondi, Blend, NFTfi, Arcade, BendDAO), the attack surface grows. Every convenience feature — bundlers, auto-repay, batch operations — is a new authorization boundary that must be independently secured.

The takeaway: When you add a new way to move assets, you're adding a new way to steal them. Audit accordingly.


DreamWork Security publishes weekly DeFi security research. Follow for vulnerability analyses, audit tool guides, and security best practices.

Top comments (0)