Ethereum Smart Contract Security Fundamentals

·

Ethereum smart contracts power decentralized applications across the blockchain ecosystem, but even small coding oversights can lead to critical vulnerabilities. Understanding these weaknesses is essential for developers and security researchers alike. This guide explores common attack vectors through hands-on examples from real-world inspired challenges, offering practical insights into securing smart contracts.


Fallback: Exploiting Receive Functions and Ownership Logic

The Fallback challenge demonstrates how improper access control and misunderstood fallback mechanisms can lead to ownership takeover. The goal is to claim ownership of the contract and drain its balance.

Key contract functions include:

Ownership is determined by whoever has the highest contribution. However, repeatedly calling contribute() isn’t feasible due to the low per-transaction limit.

Instead, attackers exploit the receive() function:

  1. Call contribute({value: 1}) to register a non-zero contribution.
  2. Send a direct transaction using sendTransaction({value: 1}), triggering receive() and transferring ownership.
  3. Call withdraw() to drain the contract.

👉 Discover how blockchain security skills can unlock advanced opportunities in Web3 development.

This scenario highlights the importance of understanding receive and fallback functions in Solidity. Since v0.6.0, these are distinct: receive() handles plain ether transfers, while fallback() handles undefined function calls or ether sends with data.


Fallout: Constructor Naming Vulnerabilities

In Fallout, the vulnerability lies in a typo within the constructor name: Fal1out instead of Fallout. Due to this single-character difference (1 vs l), the function is treated as a regular public function rather than a constructor.

As a result:

This mirrors real-world incidents like the Rubixi hack, where a renamed project failed to update its constructor, allowing an attacker to seize control. It underscores a crucial best practice: always verify constructor names match the contract exactly.


Coin Flip: Predictable On-Chain Randomness

The Coin Flip level simulates a game where players must guess ten consecutive coin tosses correctly. The outcome is based on:

uint256 coinFlip = blockValue.div(FACTOR);

Here, blockValue comes from blockhash(block.number - 1), which is predictable if the attacker controls the transaction order.

Attack strategy:

This reveals a fundamental rule: on-chain randomness is not secure unless derived off-chain or via verifiable delay functions (VDFs).


Telephone: Understanding tx.origin vs msg.sender

The Telephone challenge exploits the difference between tx.origin and msg.sender:

The condition if (tx.origin != msg.sender) allows takeover when a contract calls changeOwner(). A simple proxy contract calling this function satisfies the condition, changing ownership.

This illustrates why tx.origin should be avoided in authorization logic—it’s vulnerable to phishing attacks and proxy exploits.


Token: Integer Underflow Exploitation

The Token contract has a flawed transfer function:

require(balances[msg.sender] - _value >= 0);

Since unsigned integers cannot go below zero, subtracting a large _value causes underflow, bypassing the check.

An attacker with zero balance can transfer a massive amount (e.g., 999999999999999), resulting in their balance becoming extremely high due to wraparound.

Modern contracts use SafeMath libraries or Solidity 0.8+'s built-in overflow checks to prevent such issues.


Delegation: Delegatecall Context Confusion

Delegation uses delegatecall to forward logic to a Delegate contract. While call executes code in the target’s context, delegatecall runs it in the caller’s storage.

Calling pwn() via delegatecall modifies the delegation contract’s owner storage slot—not the delegate’s. By encoding the function signature:

web3.utils.sha3("pwn()").slice(0,10)

and sending it as transaction data, attackers trigger ownership transfer.

👉 Learn how secure coding practices protect millions in digital assets today.


Force: Forcing Ether via Selfdestruct

The Force contract has no payable functions or fallbacks—yet must hold a balance > 0. The solution? Use selfdestruct.

When a contract self-destructs, remaining ether is sent to a designated address regardless of its ability to receive funds. This bypasses all access restrictions.

An attacker deploys a contract, funds it, then calls selfdestruct(targetAddress) to forcibly inject ether.


Vault & Privacy: Bypassing Private Variables

Both Vault and Privacy store sensitive data as private, but on Ethereum, "private" only restricts direct access—not storage reading.

Using:

web3.eth.getStorageAt(contract.address, slot)

attackers extract encrypted passwords or keys directly from storage slots.

For Privacy, data is stored in a bytes32[3] array starting at slot 5. Reading slot 5 reveals the key needed for unlocking.

This reinforces that on-chain data is never truly private—encryption or off-chain storage is required for secrecy.


King: Reentrancy Without State Updates

In King, new kings must pay more than the current prize. The previous king receives payment before the new one is set.

Attackers create a contract with a reverting receive() function:

receive() external payable { revert(); }

This accepts the payment attempt but reverts it, preventing state updates—leaving the attacker permanently as king.

It shows how external interactions during state changes create denial-of-service risks.


Reentrancy: Classic Recursive Calls

The Reentrancy challenge showcases one of Ethereum’s most infamous bugs. The withdrawal process:

  1. Sends funds via .call.
  2. Then updates balances.

An attacker’s fallback function recursively calls withdraw() before balance deduction, draining funds entirely.

Mitigation: Use checks-effects-interactions pattern and OpenZeppelin’s ReentrancyGuard.


Elevator: Manipulating External Callbacks

Elevator relies on an external Building contract's isLastFloor() response. By toggling a boolean on each call, attackers trick the elevator into setting top = true.

This teaches that contracts relying on external logic must validate inputs rigorously, especially when outcomes depend on mutable responses.


Naught Coin: Bypassing Custom Modifiers

NaughtCoin overrides ERC20’s transfer() with a time-lock modifier but forgets to override transferFrom().

Attackers:

  1. Approve full balance to another address.
  2. Use transferFrom() to move tokens immediately—bypassing the lock.

Always ensure all relevant functions are secured when extending standard implementations.


Preservation: Storage Slot Confusion in Libraries

Preservation uses delegatecall to library contracts. Updating timeZone1Library with an attacker-controlled address allows malicious code execution in the main contract’s context.

Calling setTime() then writes to storage slot 0—the owner variable—due to matching storage layouts.

This emphasizes risks in upgradable patterns without proper proxy safeguards.


Recovery: Finding Lost Contracts via Etherscan

In Recovery, the SimpleToken address is lost. However, every Ethereum transaction is public.

By inspecting the creator’s transaction history on Etherscan or via RPC calls, attackers find the created contract address and call its destroy() function to reclaim funds.

Transparency enables both security and recovery—but also exposes attack surfaces.


Alien Codex: Array Index Overflow for Storage Manipulation

AlienCodex allows pushing and retracting from a dynamic array. By exploiting underflow (retract() on empty array), attackers can write beyond intended bounds.

Using arithmetic:

index = 2^256 - keccak256(slot_of_array)

they target slot 0 (owner) and overwrite it with their address via revise().

Solidity now includes bounds checks, but legacy code remains vulnerable.


Denial: Gas Depletion via Recursive Fallbacks

Denial uses .call without return value checks. An attacker sets themselves as partner and implements a fallback that recursively calls withdraw(), consuming all gas and preventing successful execution.

Robust error handling and gas stipends mitigate such DoS attacks.


Gatekeeper One & Two: Bypassing Gas and Code Checks

Both gatekeeper levels involve intricate conditions:

For Gatekeeper Two, placing logic in the constructor ensures zero runtime code size—exploiting how constructors aren't included in deployed bytecode length.


Frequently Asked Questions (FAQ)

Q: What is delegatecall used for?
A: It executes logic from another contract in the current contract’s storage context—commonly used in upgradable proxy patterns but risky if misused.

Q: Can private variables be read on Ethereum?
A: Yes. “Private” only prevents direct access; storage slots can be read externally using tools like web3.eth.getStorageAt().

Q: How do you prevent reentrancy attacks?
A: Follow the checks-effects-interactions pattern and use OpenZeppelin’s ReentrancyGuard modifier to block recursive calls.

Q: Why is on-chain randomness insecure?
A: Block data like timestamps and hashes are visible before mining, making them predictable and exploitable by miners or attackers.

Q: Is selfdestruct unstoppable?
A: Yes. It forces ether transfers regardless of recipient logic—making it a common tool for bypassing restrictions.

Q: How can I secure my smart contracts?
A: Use audited libraries (e.g., OpenZeppelin), conduct formal verification, perform third-party audits, and stay updated on known vulnerabilities.

👉 Enhance your blockchain expertise with cutting-edge security training resources now.