Solutions to Damn Vulnerable DeFi CTF challenges ⛳️
- Unstoppable
- Naive receiver
- Truster
- Side entrance
- The rewarder
- Selfie
- Compromised
- Puppet
- Puppet v2
- Free rider
- Backdoor
- Climber
The goal of the first challenge is to perform a DOS (Denial of Service) attack to the contract.
There is a suspicious line in the flashLoan
function:
uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
assert(poolBalance == balanceBefore);
If we can manage to alter the poolBalance
or the balanceBefore
, we will achieve the goal.
We can easily modify the balanceBefore
by sending some token to the pool.
In this challenge we have to drain all the funds from a contract made to call flash loans.
The contract expects to be called from the pool, which is fine, but the vulnerability lies on the fact that anyone can call the flash loan function of the pool.
In order to empty the contract in one transaction, we can create an attacker contract that calls the flash loan multiple times.
Here we have to get all the tokens from the pool, and our starter balance is 0.
The flashLoan
from the pool lets us call any function in any contract. So, what we can do is:
- Call the
flashLoan
with a function toapprove
the pool's tokens to be used by the attacker - Call the
transferFrom
function of the token, to transfer them to the attacker address
If we want to make it in one transaction, we can create a contract that calls the flashLoan
with the approve
, but instead of the attacker address, we set the created contract address. Then we transfer the tokens to the attacker in the same tx.
For this challenge we have to take all the ETH from the pool contract.
It has no function to receive ETH, other than the deposit
, which is also the attack vector.
We can create an attacker contract that asks for a flash loan, and then deposit the borrowed ETH. The pool will believe that our balance is 1000 ETH, and that the flash loan was properly paid. Then we can withdraw it.
Here we have to claim rewards from a pool we shouldn't be able to.
Rewards are distributed when someone calls distributeRewards()
, and depending on the amount of tokens deposited.
So, we can do all of this in one transaction:
- Wait five days (minimum period between rewards)
- Get a flash loan with a huge amount of tokens
- Deposit the tokens in the pool
- Distribute the rewards
- Withdraw the tokens from the pool
- Pay back the flash loan
The goal of this challenge is to drain all the tokens from the pool.
The pool has a drainAllFunds(address)
function that can only be executed by a governance address, and this is what we will be exploiting:
- Request a flash loan and get all the tokens from the pool
- Take a
snapshot
of the tokens -> Here lies one vulnerability. Anyone can take a snapshot at any time - Propose to execute an action to transfer all tokens to the attacker (the proposal will be admited since we have a lot of tokens in the snapshot)
- Return the flash loan tokens to the pool
- Wait two days (the grace period for the proposal)
- Execute the action to drain all funds
The goal here is to drain all the ETH from the exchange.
The exchange only has two methods, one to buy a token, and the other to sell it. The price is given by an oracle.
The oracle is properly initialized and only some trusted sources can update price with the postPrice
method.
The key to solve this challenge is in the misterious message from the web service:
4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35
4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34
It's some code in hex. Let's convert it to Ascii code:
MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5
MHgyMDgyNDJjNDBhY2RmYTllZDg4OWU2ODVjMjM1NDdhY2JlZDliZWZjNjAzNzFlOTg3NWZiY2Q3MzYzNDBiYjQ4
Information in the web is sometimes encoded in Base64. Let's try decoding it:
0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9
0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48
This looks like private keys! And they are. We can check the addresses they correspond to, and they are in fact the addresses from two of the trusted sources.
With these keys, we can manipulate the price of the token to buy low and sell high, extracting all the ETH from the contract.
The goal of this challenge is to drain all of the tokens from a pool.
There's a borrow
function in the pool that lets people borrow tokens for twice their price in ETH.
The vulnerability lies on the fact that it is taking the price from a Uniswap pool with very low liquidity.
So, we can lower the token price of the Uniswap pool by swapping some ETH to tokens.
Then, when the price is low enough, we can borrow all the tokens from the pool for a very low price.
This challenge has the same issues as the previous one. The price from the pool relies on a single oracle that can be attacked to change the price.
The attack is the same as in the previous challenge, but instead of interacting with Uniswap v1, in this case its v2:
await this.token.connect(attacker).approve(this.uniswapRouter.address, ATTACKER_INITIAL_TOKEN_BALANCE);
const deadline = (await ethers.provider.getBlock("latest")).timestamp * 2;
await this.uniswapRouter
.connect(attacker)
.swapExactTokensForETH(
ATTACKER_INITIAL_TOKEN_BALANCE,
0,
[this.token.address, this.weth.address],
attacker.address,
deadline,
{ gasLimit: 1e6 },
);
const tokens = await this.lendingPool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE);
await this.weth.connect(attacker).deposit({ value: tokens });
await this.weth.connect(attacker).approve(this.lendingPool.address, tokens);
await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE);
The goal of this challenge is to send some NFTs from a vulnerable marketplace to a buyer.
The marketplace contract has a bug here:
// transfer from seller to buyer
token.safeTransferFrom(token.ownerOf(tokenId), msg.sender, tokenId);
// pay seller
payable(token.ownerOf(tokenId)).sendValue(priceToPay);
It was supposed to be paying the seller, but in reality, it is paying the new owner, which is the buyer.
On top of that it has another bug. If you buy multiple NFTs, you only have to pay for one:
require(msg.value >= priceToPay, "Amount paid is not enough");
That line is executed multiple times, but with the same msg.value
.
The only thing we need to do to perform the attack is get a flash loan from Uniswap, and repay it after getting the reward.
The goal of this challenge is to take all the tokens from a Gnosis Safe wallet.
The proxy wallets haven't been initialized. So, it may be possible to execute some code to exploit that.
If we create a proxy wallet with GnosisSafeProxyFactory.createProxyWithCallback
, it initializes the wallet internally, and it is possible to execute code in the context of the proxy (using its storage).
When the proxy wallet is initialized, it calls the WalletRegistry.proxyCreated
function. That function has a lot of requirements to prevent it being executed by anyone except for the master copy of the wallet contract, plus other validations, making it impossible to transfer the tokens tricking it.
But, what we can do is initialize the wallet with an initializer that calls another contract that approves and spender to use the tokens. It uses the proxy context, so it allows its own tokens.
Then after the proxy is created and initialized, we can simply transfer the tokens, as we have previously set our address to be the spender.
Solution from this post
The goal of this challenge is to empty the vault.
The schedule
function does not work as expected an is vulnerable to an attack. It first executes the tasks and later checks if they were previously scheduled. So we can use this for our advantage.
We must perform a series of tasks to empty the vault: First we have to set the ClimberTimelock
delay
to 0 and grant the proposer
role to the attacker contract. Then we can transfer the ownership of the contract to the attacker. All of this must be done via tasked passed to the contract function.
We can then upgrade the contract to a new version that modifies the sweepFunds
function to allow the attacker to call it.
Call the sweep and it's done.