In this post, I will share my explanations for DamnVulnerableDeFI challenges 6 through 10.
You can find the code here.
Let’s begin.
This took me a long time to solve, the main reason being unfamiliarity with the GnosisSafe contracts. So don't take this explanation as a fact. It did involve a lot of trial and error, and maybe luck was a key factor.
First things first - the challenge consists of 4 users that upon creating a GnosisSafe wallet according to the WalletRegistry specifications will receive 10 DVT tokens. Our goal is to steal them all in one transaction. This is our first clue, we will need a smart contract to do the dirty work.
Since I did not know the GnosisSafe contracts, I started by checking the setup for this challenge. Unexpectedly, it is quite easy. There are four contracts: WalletRegistry, GnosisSafeProxyFactory aka walletFactory, GnosisSafe aka masterCopy, and the DamnValuableToken contract. We also know who the allowed users are.
Next, I decided to look through the contracts. GnosisSafeProxyFactory and GnosisSafe contracts do not contain code exploits as far as I can tell. WalletRegistry was the only contract left to check, and after spending more time than I care to admit looking for code exploits, I couldn't find one in this contract either.
Not sure what to do, I decided to list what I found out while looking through the contracts:
We know that
WalletRegistryis the contract that has the tokens, so we'll have to execute theproxyCreated(...)function to get them;The
proxyCreated(...)function makes sure that the wallet was created using theGnosisSafesetup(...)function;To execute the
proxyCreated(...)function we have to create the wallet usingGnosisSafeProxyFactory createProxyWithCallback(...)function;
I decided to take a deeper look at the createProxyWithCallback(...) function, which has to be executed first. According to the comments, createProxyWithCallback(...) allows us to create a new GnosisSafeProxy and call it after it is initialized. It also allows us to call a specified callback function afterward.
From that, I assumed two things: the callback must be the WalletRegistry proxyCreated(...) function and that this is how we reach the GnosisSafe setup(...) function.
After making the above assumptions, I decided to look at the setup(...) function. I noticed two more things: some parameters weren't important and it also allows us to perform a delegatecall to a specified address.
This looked promising. If I'm able to successfully pass the verifications on the WalletRegistry proxyCreated(...) function, I can then execute a callback defined by me on the proxy itself, which by now will have the 10 DVT according to the WalletRegistry proxyCreated(...) function.
All that was left was implementing the contract. Most of it was pretty easy, but I was having trouble making a correct call to the setup(...). The initializer variable passed to the createProxyWithCallback(...) must contain the setup(...) function selector and parameters. Most of it can be ignored, but I was also ignoring the threshold parameter - which was a mistake. The proxyCreated(...) callback verifies that, so don't make the same mistake as I did.
I realize that this isn't a very clear explanation. It might even contain errors. But it was most of my thought process until finally reaching a solution. I rather find a solution myself even if it involves luck and trial and error than simply copy someone else's. Besides, after solving this I looked online for explanations and couldn't find much better explanations. I did find contract improvements though, so there's that.
he goal of this challenge is clearly stated: steal 10 million DVT from the ClimberVault contract.
We are also given a couple of extra hints:
The
ClimberVaultis upgradeable, following theUUPSpattern;ClimberVaulthas the role ofSweeperwhich can sweep all funds from the vault;The owner of
ClimberVaultis theClimberTimelockcontract;The
ClimberTimelockcontract can withdraw a limited amount of tokens from the vault every 15 days;The
ClimberTimelockcontract allows accounts with theProposerrole toscheduleactions that will be executed 1 hour later;
Since we are given so much information, I instantly assumed this would be a tough challenge.
First I used my very limited knowledge of the UUPS pattern to look for a pattern error in the ClimberVault contract. However, it does have an empty constructor and instead is initialized by a initialize function. This is not the way in.
I then tried to find a way to get the Sweeper role. I couldn't find any. If you can, let me know.
However, while looking through the ClimberTimelock contract, I did notice that it has the role of Admin of itself, meaning it can grant the roles defined. I also noticed that anyone can call execute() to execute a proposal, as long as it has been scheduled. This function contains an enormous error, it only checks that a given proposal was ready for execution after executing it!
This is the point at which I knew I was onto something. Theoretically, we could create a contract to execute a proposal that grants itself the Proposer role and transfers the ownership of the ClimberVault to an account controlled by us. After that, it would be a simple matter of creating a new ClimberVault that allows us to call the sweepFunds() function, and upgrading the proxy to use our new ClimberVault.
While implementing the proposal I forgot one thing: we know from the challenge description that the delay to execute a proposal is 1 hour. Fortunately, we can just set it to 0 without any issue.
Also, don't forget that even though the checks-effects-interaction pattern is violated, the ClimberTimelock does check that the proposal is ready to be executed, so we'll have to schedule it otherwise it will revert and the attack won't work. Plus, the proposal can't call the schedule() function itself because the parameters won't match. So, we need to set a task that calls a function in our attacker contract that schedules the task, which will work because we already have the Proposer role.
So, to recap:
Implement a
ClimberVaultversion that allows us to call thesweepFunds()function;Create a contract that executes a proposal with the following tasks:
Set the delay to 0;
Grant the contract the proposer role;
Transfer the ownership of the vault to our attacker account;
Schedule the proposal;
Execute the proposal;
Update the proxy to use our
ClimberVaultSweep the funds;
This is not an explanation of the solution, this is an unfiltered account of how I reached a solution. This is because I don't really understand this challenge. Then again, even the challenge description says it will be modified for a future version of damnvulnerabledefi.
With that caveat, let me explain how I reached a solution.
From the description, I immediately assumed I had to deploy a contract using the attacker to the specified address, because it clearly says that the address that has the tokens is empty.
My first instinct was to use CREATE2, but then it occurred to me it probably would take eternities to find the correct salt to get that same address.
So I decided to try brute forcing it first using CREATE, since paying for gas is not a concern in this scenario.
My first attempt consisted of having the attacker deploy 100k contracts that just transferred the DVT tokens to the attacker. Not only did this take a long time, but it also didn't work.
Since I am not sure how hardhat deploys contracts, I decided to give it another shot using a SafeMinersAttackerFactory to deploy the SafeMinersAttacker before giving up.
At first, I tried having the factory deploy 1000 instances of the attacker until the attacker account had the tokens. This ran into an out-of-gas error because I, naively, forgot that there's a limit to the gas I can send to the contract.
So I decided to modify how this would work. The attacker account would deploy a SafeMinersAttackerFactory that would create 500 instances of the SafeMinersAttacker. Then, if the attacker had the tokens, I would stop, otherwise, I would have the attacker deploy another factory.
I was expecting this to run for a while, but since I had to leave my computer at the time it was cool. If it didn't work, I would know brute force was the wrong approach. To my surprise, it worked on the second batch of 500 SafeMinersAttacker deployments.
