πŸ“”Storyline

Setup level 1

Clone the challenge repository (branch v3.0.0):

git clone https://github.com/tinchoabbate/damn-vulnerable-defi.git --branch v3.0.0

Open the damn-vulnerable-defi folder as a VSCode project and locate the following files:

.
β”œβ”€β”€ contracts
β”‚Β Β  β”œβ”€β”€ unstoppable
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ ReceiverUnstoppable.sol    // Scoped contract code
β”‚Β Β  β”‚Β Β  └── UnstoppableVault.sol       // Scoped contract code
β”œβ”€β”€ package.json                       // Node alias to run the Unstoppable test
β”œβ”€β”€ test
β”‚Β Β  β”œβ”€β”€ unstoppable
β”‚Β Β  β”‚Β Β  └── unstoppable.challenge.js   // Where we interact with the contract / poc

Install dependencies and run the challenge to verify it works:

yarn install
yarn unstoppable

A correct output at this part would be:

  [Challenge] Unstoppable
    βœ” Execution
    1) "after all" hook for "Execution"
    
  1 passing (3s)
  1 failing

  1) [Challenge] Unstoppable
       "after all" hook for "Execution":
     AssertionError: Expected transaction to be reverted  

If you are confused take a minute to understand this part:

  • yarn run translates to yarn hardhat test test/unstoppable/unstoppable.challenge.js

  • unstoppable.challenge.js imports chai, which is a JavaScript assertion library, meaning it helps developers write test scripts for their code.

  • chai's expect is used to ensure a certain value is as the developer expected, for example expect(1).to.equal(2) will trigger a test fail.

  • Usually failed tests mean code is broken or bugged, but in this case it's intentional to indicate challenge fail/success.

Understanding the challenge objective

In our unstoppable.challenge.js test file the authors had prepared this logic:

const { ethers } = require('hardhat');
const { expect } = require('chai');

describe('[Challenge] Unstoppable', function () {
    let deployer, player, someUser;
    let token, vault, receiverContract;

    // redacted..

    before(async function () {
        /** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */

        [deployer, player, someUser] = await ethers.getSigners();

        // redacted..
        receiverContract = await (await ethers.getContractFactory('ReceiverUnstoppable', someUser)).deploy(
            vault.address
        );
        
        // redacted..
    });

    it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
    });

    after(async function () {
        /** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */

        // It is no longer possible to execute flash loans
        await expect(
            receiverContract.executeFlashLoan(100n * 10n ** 18n)
        ).to.be.reverted;
    });
});

We can see some setup code that's mostly redacted, and unit test syntax with hooks that control execution order.

  • before is triggered before thee generic tests, initializing the environment properly.

  • it defines a simple test case to run.

  • after triggers after other tests had finished.

To put it more simply - our test fails whenever that a call to executeFlashLoan from the ReceiverUnstoppable.sol contract is reverted.

A reverted contract in the context of Ethereum means that the contract call/transaction was unsuccessful and the state changes made in the current scope of the contract were undone.

What we can deduce from this is that the challenge author expects us to basically break the ReceiverUnstoppable.sol contract so that every time a executeFlashLoan is called, it won't succeed.

Fake Product Landing Page / Info

In a proper audit, rather than a challenge, you'd have some marketing data / protocol RFC / whitepapers to research about before diving into the code.

Imagine the following as public information found about the solution.


Welcome to Unstoppable Vault, the cutting-edge protocol where financial innovation meets security and efficiency in the DeFi space.

The Unstoppable Vault offers a Lending Pool that contains a million DVT tokens. The pool offers a grace period allowing Flash Loans without any fees!

Our protocol is designed to empower users with the dynamic tool of flash loans, that allows for borrowing and returning funds within a single transaction.

Whether you're a seasoned DeFi enthusiast or a curious newcomer, UnstoppableVault offers an unprecedented opportunity to leverage assets in ways traditional finance never could.

Key Features:

  • Flash Loan Functionality: Borrow any available amount of assets from our vault instantly without collateral. It’s fast, efficient, and opens up a world of possibilities for arbitrage, collateral swapping, self-liquidation, and more.

  • Innovative Fee Structure: Enjoy competitive fees with our transparent fee model. Our fees are designed to be fair, fostering an environment conducive to both short-term operations and long-term financial strategies.

  • User-Friendly Interface: Access flash loans with ease thanks to our intuitive platform. Whether you're a developer or an investor, our user-friendly interface makes navigating the world of flash loans straightforward and hassle-free.

  • Community-Driven Development: At UnstoppableVault, we believe in the power of community. Our protocol is continuously evolving, with improvements and new features driven by the feedback and needs of our users.

  • ERC4626 Integration: Leveraging the latest in DeFi standards, UnstoppableVault is built on ERC4626, offering a tokenized vault standard for a seamless DeFi experience.

You were called for a quick scoped audit on the Unstoppable protocol.

The scope is as follows:

In the scoping call, the client had described the product is especially reliant on these token contracts standards:

The client mentioned that they were previously under a Force Feeding attack, and are most scared about someone bricking/stopping the flash loans functionality.

Please review the code and let us know if you discover any risks!

Last updated