Capture The Ether Math Solutions

In this post, I will share my explanations for the Math section of the Capture The Ether challenges. There are plenty of solutions around the web - my goal was to solve the challenges locally, avoiding Etherscan when possible, and writing code locally.

You can find the code here.

Let’s begin.

Token Sale

Looking at the contract, it may seem like everything is correct. However, if you look closely, you'll see that this contract does not implement SafeMath.

Remember, SafeMath is implemented on the language level since solidity version 0.8. This challenge uses an older version, so there's the possibility of overflows. This is how we game the contract.

If you look at the buy function, require(msg.value == numTokens * PRICE_PER_TOKEN) has the potential to overflow. Overflowing here would allow us to get a gigantic amount of tokens for a low price. We then sell 1 token for 1 ether, making a profit on the way and leaving the contract with a balance < 1, which is the required condition for us to solve the challenge.

Once again, the code is explained. Still, here are the steps needed:

  1. Get the contract abi and address;

  2. Get the private key of the ropsten account you are using to interact with Capture The Ether. Otherwise, you can't pass the challenge as CTE doesn't know who you are;

  3. Get the contract and connect with it using your account;

  4. Determine the amount of tokens to send to cause an overflow;

  5. Determine the amount of ether to send;

  6. Buy our tokens and wait for them to arrive;

  7. Sell 1 token;

  8. Profit;

To keep this description short, steps 4 and 5 are detailed in the tokenSale.js script. Take a look.

Token Whale

Looking at this contract, you'll hopefully notice that, once again, it is subject to over and underflows. However, this is part of the solution. Not all of it.

If you look at the _transfer function, it spends msg.sender tokens instead of tokens from the from address passed to the transferFrom function. This means that, if we successfully call transferFrom from an account with 0 tokens, balanceOf[msg.sender] -= value will underflow, causing the said account to receive a gigantic amount of tokens - 2**256 - 1 to be exact.

After that, all we need is to send 1000000 tokens from the helper account to our account, and we'll have successfully solved the challenge.

You may be thinking "Why do I need a second account? Couldn't I do this just using my account?". You couldn't. You'd never pass the require(balanceOf[from] >= value) check, as you'd need value to be 1001 to cause an underflow.

Once again, the code is explained. Still, here are the steps needed:

  1. Get the contract abi and address;

  2. Get the private key of the ropsten account you are using to interact with Capture The Ether. Otherwise, you can't pass the challenge as CTE doesn't know who you are;

  3. Get a second ropsten account;

  4. Approve the second account to spend funds from your account. Sign this transaction using your main account;

  5. Transfer 1 token from your main account, to that same account. Sign this transaction using the second account. This will cause the underflow and give the second account an enormous amount of tokens;

  6. Send the required 1000000 tokens from the second account back to your main account. Sign this transaction using the second account.

  7. Wait for them to arrive, and you'll have solved the challenge.

Retirement Fund

Looking at this contract, you'll hopefully notice that, once again, it is subject to over and underflows. However, this is part of the solution. Not all of it.

If you look at the contract, you'll see that withdraw checks that msg.sender == owner, since the owner is the CTE factory, this function is of no use to us. Meaning, that our solution can only make use of the collectPenalty function.

As we've established, the contract is subject to over and underflows, meaning that uint256 withdrawn = startBalance - address(this).balance; can be underflowed, allowing us to drain all the ETH the contract has.

To achieve this, address(this).balance has to be > 1, which in turn means that we'll have to find a way to add some ether to the contract.

The contract has no payable functions, so how can we send ether to the contract? If you look through the Ethereum documentation, you'll hopefully realize that the easiest way to do this is to self-destruct another contract that has some ether in it, and send that contract ETH to the CTE challenge contract.

Once again, the code is explained. Still, here are the steps needed:

  1. Get the contract abi and address;

  2. Get the private key of the ropsten account you are using to interact with Capture The Ether. Otherwise, you can't pass the challenge as CTE doesn't know who you are;

  3. Get the contract and connect with it using your account;

  4. Deploy our helper contract. Send 1 eth as the msg.value. Don't forget to add the challenge address to the contract kill function;

  5. Destroy our helper contract;

  6. Call the challenge contract collectPenalty function;

  7. Wait for challenge contract to be drained and you'll have solved the challenge

Mapping

To solve this challenge, we need to somehow set isComplete to true, or 1.

First, you should read the Ethereum docs to understand how contract storage works. You'll hopefully reach the conclusion that isComplete is at slot 0. Then, slot 1 has the map[] length.

From the documentation, we can also gather that: keccak256(1) has the map[0] value, keccak256(1) + 1 has the map[1] value and so forth. This is because the contract array is a dynamic size array, so the EVM reserves one slot to store the array length.

From this, we can expect that if we set the map length to 2**256 - 1, the slot that contains the isComplete value will be occupied by the array, allowing us to modify it if we know the corresponding storage address.

Since we know that map[0] is at keccak256(1), we also know that map[isComplete] = 2**256 - keccack256(1).

Once again, the code is explained. Still, here are the steps needed:

  1. Get the contract abi and address;

  2. Get the private key of the ropsten account you are using to interact with Capture The Ether. Otherwise, you can't pass the challenge as CTE doesn't know who you are;

  3. Get the contract and connect with it using your account;

  4. Expand the array bounds to occupy the isComplete slot;

  5. Determine the storage address of the isComplete value;

  6. Change it to 1 (true);

Donation

Looking at the contract, you'll hopefully notice two things almost immediately: the donate function calculates scale wrong, as it results in 10**36 since 1 ether == 10**18 already, and that we'll need to somehow call withdraw to drain the contract.

Taking a deeper look, the withdraw function requires us to be the contract owner, so we know what our goal is: become the contract owner.

If you read the storage layout docs in the previous challenge you'll remember that struct and array data use their own slots. Since the contract struct is only initialized when the donate function is called, we can assume that slot 0 has the Donation[] size and that slot 1 has the owner address. So, we must write to slot 1 of the contract storage, but how?

This is where the donate function is wrong again. It declares Donation donation without using either the memory or storage keywords, which means that this is just an uninitialized pointer to the contract storage. Since the struct has to uint256 values, etherAmount will write to slot 1, where the owner address is stored! All we need to do is determine the uint256 value of our address and send that as the etherAmount, with the needed msg.value to pass the require check.

Once again, the code is explained. Still, here are the steps needed:

  1. Get the contract abi and address;

  2. Get the private key of the ropsten account you are using to interact with Capture The Ether. Otherwise, you can't pass the challenge as CTE doesn't know who you are;

  3. Get the contract and connect with it using your account;

  4. Determine the uint256 value of our address;

  5. Calculate the needed msg.value;

  6. Make the donation;

  7. Withdraw the eth;

Fifty Years

This is the challenge that awards the most points for a reason. It requires an orderly combination of transactions, with the intent to exploit the contract using the techniques we've used in the previous challenges.

First, let's try to determine our goal. Looking at the withdraw function, particularly the second require check, we can see that we need to send an index for a contribution that has an unlockTimestamp in the past and corresponds to the last contribution made, so we drain all the contributions. This is our goal. Now, how do we do this?

It should be clear that the upsert function is key. If we look through its code, we can identify two potential issues: require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days) is subject to overflows, and the else statement relies on a previous declaration of contribution, which makes it an uninitialized storage pointer, which allows us to access storage slots 0 (queue.length) and 1 (head) (remember this from the previous challenge?). Knowing this, we can determine two steps needed:

  • Call upsert once with a new contribution designed to prepare an overflow of the timestamp when we make the second contribution, allowing the second contribution to have timestamp = 0. Since the contribution.unlockTimestamp = timestamp writes to the head storage slot, after this first contribution head will have a gigantic value.

  • Make another upsert call that resets head to 0, while also having an unlockTimestamp == 0, which will work, because we've prepared the overflow in the first upsert call.

This will total, 3 contributions (adding to the one that is made when we begin the challenge on CTE). There are three things we need to pay attention to while executing the two contributions:

  • Time units are parsed to seconds, so we need to prepare the overflow taking that into account;

  • Ether units are handled in wei, so if we send ETH to the contract the actual queue.length will be x ETH * 10**18. So we need to send wei, not ether as the msg.value;

  • queue.push increments the queue.length before actually inserting the contribution. We've determined that the queue.length will be manipulated by the line contribution.amount = msg.value. This means that if we want to keep an accurate queue.length value, we need to be wary of the msg.value we send with each upsert call. We know that before our first upsert call the queue.length is 1, because one contribution was made when we began the challenge. However, we need to send 1 wei in the first contribution because, even though the line contribution.amount = msg.value will maintain the queue.length at 1 (when it's actually 2 since this is the second contribution made), the line queue.push(contribution) will increment it by 1, which will give us the correct queue.length of 2. We follow the same logic for the second upsert call and send 2 wei, since 2 + 1 = 3, which by then will be the correct queue.length.

At first glance, we may assume that, after these steps, calling withdraw(2) would drain the contract. However, this transaction would fail. Since queue.push increments the queue.length before actually pushing the contribution, contribution.amount = msg.value will be incremented too. Visualize it this way:

- Contribution 0 (made by CTE): contribution.amount == msg.value == 1 ETH;
- Contribution 1 (us): contribution.amount == msg.value == 1 wei + `queue.push` == 2 wei;
- Contribution 2 (us): contribution.amount == msg.value == 2 wei + `queue.push` == 3 wei;
- Contract total == 1.00...03 ETH, Contributions total == 1.00...05 ETH.

Hence, the transaction will fail until we add 2 wei to the contract, because we're trying to withdraw more ETH than the contract has. This can be done by taking a page out of the Retirement Fund challenge. Just create a contract that receives 2 wei on deploy, and then self-destruct said contract, sending the 2 wei to our challenge contract. After that, call withdraw(2) and we win!

Once again, the code is explained. Still, here are the steps needed:

  1. Get the contract abi and address;

  2. Get the private key of the ropsten account you are using to interact with Capture The Ether. Otherwise, you can't pass the challenge as CTE doesn't know who you are;

  3. Get the contract and connect with it using your account;

  4. Deploy our FiftyYearsHelper contract and fund it with 2 wei;

  5. Call upsert a first time to prepare the timestamp overflow;

  6. Call upsert a second time to set head to 0;

  7. Kill our FiftyYearsHelper contract;

  8. Call withdraw(2);

Subscribe to kyrers
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.