Math

Token sale

Writeup

pragma solidity ^0.4.21;

contract TokenSaleChallenge {
    mapping(address => uint256) public balanceOf;
    uint256 constant PRICE_PER_TOKEN = 1 ether;

    function TokenSaleChallenge(address _player) public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance < 1 ether;
    }

    function buy(uint256 numTokens) public payable {
        require(msg.value == numTokens * PRICE_PER_TOKEN);

        balanceOf[msg.sender] += numTokens;
    }

    function sell(uint256 numTokens) public {
        require(balanceOf[msg.sender] >= numTokens);

        balanceOf[msg.sender] -= numTokens;
        msg.sender.transfer(numTokens * PRICE_PER_TOKEN);
    }
}

The constant PRICE_PER_TOKEN is 1 ether, which is represented by 10**18 wei in Solidity. This is a really large number.

In the function buy():

buy() overflow

We know that integer overflow/underflow is a thing for old versions prior to Solidity 0.8. Since PRICE_PER_TOKEN is huge and we have control over numTokens, we can pick a suitable numTokens and make numTokens * PRICE_PER_TOKEN overflow.

How large numTokens is supposed to be? Let's do the math:

Solution

  1. Copy and paste the challenge contract into Remix and interact with it via "At Address".

  2. Call buy(115792089237316195423570985008687907853269984665640564039458) and send 415992086870360064 wei as msg.value.

  3. Call sell(1).

  4. Call isComplete() to verify if the challenge was successfully solved.

Token whale

Writeup

The transferFrom(from, to, value) function calls _transfer(to, value):

This implementation is wrong. In fact, it deducts balance from msg.sender instead of from. Moreover, balanceOf[msg.sender] -= value has underflow problem.

Solution

Here is the attack plan:

  • Initially we have balanceOf[player] = 1000.

  • Create a proxy account in Metamask, call it backup.

  • player calls transfer(backup, 510). Here "510" can be any number greater than 500. If we transfer 510 tokens to backup, we now have 490 in player and 510 in backup.

  • backup calls approve(player, 500). This sets the allowance and prepares for transferFrom().

  • player calls transferFrom(backup, backup, 500). In this step, _transfer(backup, 500) is being called. Since _transfer() deducts balance from msg.sender instead of from, the player's account will be deducted 500. Recall that player's balance is 490 at this moment, so balanceOf[msg.sender] is going to underflow to a huge number. That is, the player account has a huge balance now.

  • Call isComplete() to verify if the challenge was successfully solved.

Retirement fund

Code Audit

The collectPenalty() function has underflow problem:

collectPenalty() underflow

Here startBalance is 1 ether, and the developer assumed that address(this).balance <= 1. This assumption is clearly false since an attacker can call selfdestruct() in a proxy contract to forcefully send ether to the challenge contract. This will make address(this).balance > startBalance, so startBalance - address(this).balance < 0 and it causes underflow. withdrawn will be a huge positive number.

Solution

Write a selfdestruct contract and deploy it with msg.value == 1 wei:

After that, copy and paste the challenge contract into Remix and interact with it via "At Address". Call collectPenalty(). This step must be done manually since collectPenalty() checks require(msg.sender == beneficiary) and we have beneficiary = player in the constructor. Call isComplete() to verify that the challenge was solved successfully.

Mapping

Code Audit

uint256[] map is a dynamic array stored in storage:

In the case of a dynamic array, the reserved slot p contains the length of the array as a uint256, and the array data itself is located sequentially at the address keccak256(p).

Read more about storage:

Understanding Ethereum Smart Contract Storage

In our case, the storage layout is:

Recall that the max storage slot is 2**256 - 1 and the next slot after that is just slot 0 because of the overflow.

Solution

Let's do a bit of math:

  • map[0] is at slot keccak(1).

  • Max storage is at slot 2**256.

  • We need to overwrite slot 2**256 - keccak(1), which is just slot 0.

  • keccak(1) == 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6

  • 2**256 - 1 == 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

  • 2**256 - keccak(1) = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 + 1

Compuet 2**256 - keccak(1) in Python:

Call set(35707666377435648211887908874984608119992236509074197713628505308453184860938,1) to overwrite isComplete.

Donation

Code Audit

Note that when the struct donation is declared, its location is not defined (memory or storage). In this case, Solidity picks storage by default. Since donation is uninitialized, it will be pointing to slot 0 and slot 2 (this struct has two entries, each entry is 32-byte long). Specifically, donation.timestamp points to slot 0 and donation.etherAmount points to slot 1.

Learn more about storage:

Understanding Ethereum Smart Contract Storage

Solution

Let's anaylze the storage of this contract:

  • slot 0: since Donation[] public donations is a dynamic array, its length is stored in this slot.

  • slot 1: address public owner is stored here.

When the following code snippet is executed:

Since donation is uninitialized, donation.timestamp overwrites slot 0 and donation.etherAmount overwrites slot 1. That is, we can overwrite owner with donation.etherAmount.

Now our task is choosing a suitable donation.etherAmount. This value is uint256, but we want it to represent an address. Note that there is another bug in the donate() function:

donate()

Since ether is just an alias of 10**18, this code is equivalent to:

In this way, msg.value will be a small number, definitely less than 1 ether. Here you can convert your Metamask wallet address to uint256, compute msg.value == <converted_address> / 10**36 and call donate(<converted_address>) together with the msg.value you just computed. Once owner is overwritten, call withdraw().

Fifty years

Last updated