Skip to main content

Command Palette

Search for a command to run...

The One With an Unstoppable Vault

Updated
6 min read
The One With an Unstoppable Vault
P
I'm a [Web2 & Web3] security researcher. Passionate about breaking (and fixing) things on-chain & off-chain. I live and breathe code, music, and video games. Altruistic by nature — always happy to connect, learn, and grow together!

Welcome to the first post in the Damn DeFi series — a hands-on journey through DeFi security challenges inspired by Damn Vulnerable DeFi. If you're passionate about Web3 security, Solidity, and breaking things for good reasons — you're in the right place.

👋 Who am I?

I'm kode-n-rolla, an offensive security researcher diving deep into the world of smart contract security. Through this series, I’ll be sharing practical walkthroughs of vulnerable contracts, exploits, and techniques for testing DeFi systems.

Whether you’re an aspiring auditor or a curious hacker, I’ll guide you through each challenge step-by-step — from understanding the code to developing working exploits and verifying success criteria.

🔨 What you'll need

To follow along, make sure you have:

List of the Episodes:

The One Where Doing Nothing Costs Money

The One With Too Much Trust

The One With a Side Entrance

The One With Temporary Authority


Unstoppable

This is the first challenge in the Damn DeFi set — and it’s a great warm-up to get into the mindset of a smart contract auditor.

We’ll explore a seemingly solid ERC4626 vault contract that offers flash loans, as well as a monitoring contract that reacts if something goes wrong. Our goal is to break the flash loan functionality by introducing a state inconsistency that will prevent further usage of the feature — triggering a pause in the vault and returning control to the deployer.

In the next section, we’ll break down the logic, identify the vulnerability, and develop a working solution using Foundry.


Stay tuned — the vault may be called Unstoppable, but we’re about to prove otherwise.

Overview: What’s Unstoppable?

The Unstoppable Vault is a tokenized vault (ERC4626) holding 1,000,000 DVT tokens. It provides free flash loans during an initial 30-day grace period (GRACE_PERIOD), aiming to test functionality before going fully permissionless.

To monitor this behavior, a dedicated UnstoppableMonitor contract can programmatically test the flash loan system. If something breaks (e.g., flash loan reverts), it will:

  • 🚨 Pause the vault

  • 🔐 Transfer ownership to the original deployer

The vault is considered "unstoppable" because, in theory, it should always allow flash loans as long as the system is balanced.

But... there’s a catch.


🎯 Objective: How Do You Break It?

You start as the player with 10 DVT tokens and no ownership or control. Your goal is to:

Make the vault fail its flash loan check.

If you succeed, the monitor contract will trigger emergency pause, halting the vault and returning ownership to the deployer.

✅ To mark the challenge as solved:

  • Flash loan must revert.

  • Vault must be paused.

  • Vault must transfer ownership back.

Why the Vault Can Be Broken

Let’s understand why the vault’s logic is vulnerable.

The core of the problem lies in this check inside the flashLoan() function:

uint256 balanceBefore = totalAssets();
if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();

This line assumes that the vault’s balance (totalAssets()) is always in sync with the total supply of tokens (totalSupply()). But that’s a dangerous assumption.

🕳️ Here’s the trap:
The totalAssets() function just checks how many DVT tokens the contract holds.
The totalSupply() tracks how many shares were minted through proper deposits.

Now imagine this:

  • Someone sends DVT tokens directly to the vault without calling deposit().

  • The vault’s totalAssets() increases.

  • But totalSupply() stays the same.

Boom — desynchronization 💥


🧠 Visualization: How the Vault Breaks

[MONITOR]
   │
   ▼
checkFlashLoan()
   │
   ▼
vault.flashLoan(...)  ⟶ checks:
                        - is msg.sender == monitor? ✅
                        - totalAssets == totalSupply? ❌ ← We break this
                             |
                             └── revert InvalidBalance
                                      │
                                      ▼
                              monitor.pauseVault()
                              - vault.paused = true
                              - vault.owner = deployer

💥 BOOM — The vault is no longer unstoppable.

💡 The key idea is to desynchronize totalAssets() and totalSupply() by directly transferring DVT tokens into the vault (not via deposit()), which increases totalAssets but not totalSupply.

Test That Breaks the Vault

To confirm the vulnerability, here’s the test written in Foundry:

function test_unstoppable() public checkSolvedByPlayer {
    token.transfer(address(vault), INITIAL_PLAYER_TOKEN_BALANCE);

    vm.expectRevert();
    vault.flashLoan(monitorContract, address(token), 1e18, "");
}

Let’s break it down step by step 🧩:

  1. token.transfer(address(vault), INITIAL_PLAYER_TOKEN_BALANCE);
    → We send tokens directly to the vault without using the deposit() function.
    → This breaks the synchronization between totalAssets() and totalSupply().

  2. vm.expectRevert();
    → We expect the next call to fail (revert). It’s a feature of Foundry tests to check if an error is thrown.

  3. vault.flashLoan(...)
    → This triggers the flash loan. But remember:
    totalAssets() != totalSupply() → 💥 FLASHLOAN_DISABLED is reverted!

🧠 What Went Wrong?

This is a great example of how unexpected token flows can break carefully crafted invariants in smart contracts.

In our case, the UnstoppableVault assumes that all token deposits happen only through its own deposit() function — where totalSupply() and totalAssets() remain in sync.

But in reality, ERC20 tokens don’t prevent direct transfers. That means any user can bypass the contract's logic by sending tokens directly to the vault’s address.

This subtle assumption leads to a broken invariant:

if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();

Once this condition fails, the flash loan feature becomes unusable — and the monitoring contract pauses the vault. Game over 🕹️


🚨 Real-World Impact

In a real DeFi protocol, this kind of bug could:

  • Permanently disable an essential feature, like flash loans or interest accrual

  • Lead to locked funds or loss of protocol utility

  • Trigger panic, loss of user trust, or even exploit if other invariants rely on this sync

It's not just a toy bug — it's a critical design flaw rooted in false assumptions about ERC20 behavior.


🔧 How to Fix It?

To prevent this issue, developers should:

  • Never rely on assumptions like totalAssets() == totalSupply() staying true

  • Instead, calculate totalAssets() using actual token balance:

      return token.balanceOf(address(this));
    
  • Or redesign the logic to not rely on that invariant at all

In short: always expect users to interact with your contract in unpredictable ways 🤯

🔍 Why This Matters for Auditors and Researchers

This type of issue perfectly illustrates the importance of smart contract audits — not just for spotting obvious bugs, but for challenging dangerous assumptions baked into the logic.

Even seemingly simple lines like:

if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();

...can lead to critical vulnerabilities if the protocol design doesn’t account for all possible token interactions.

As security researchers, our job is to think like attackers while staying on the defender’s side. Break things to make them stronger 🛡️


🙏 Thanks for Reading!

Thanks for following me through this challenge!
If you're learning smart contract security or just love breaking things for the greater good — stick around 💥

You can find me sharing more write-ups, tips, and experiments on:

Happy hacking — and see you in the others episodes of the Damn DeFi series 🚀
Until then, keep questioning assumptions and pushing boundaries!

U

Amazing how such a small assumption can completely break a system that looks solid on the surface. The walkthrough really highlights why understanding token flows beyond the “intended path” is such an important part of auditing. Excited for the next episode, these deep dives hit the perfect balance of clear and practical.

1
P

Thank you so much for your kind words! I really appreciate it.

Sorry for the late reply - I somehow missed your comment while being deep in work and other projects.

I’m really glad you enjoyed the walkthrough. That was exactly the point I wanted to highlight: sometimes a small assumption in the token flow can break the whole system, even if everything looks solid on the surface.

There are a few more episodes in my profile, so feel free to check them out. I’d be happy to hear your thoughts on them as well.

Always glad to discuss security, auditing, and development topics!

Damn Vulnerable DeFi

Part 2 of 5

Solving Damn Vulnerable DeFi challenges one hack at a time 🧠 Each post walks you through how I exploit smart contract vulnerabilities and what we can learn from them.

Up next

The One With a Side Entrance

A subtle accounting bug where deposits and repayments blur — letting attackers slip in through a “side entrance”.