Solidity BasicsPart 3: Advanced Patterns
Series: Web3 Security Zero se Advance 🛡️ | Article #7 By HackerMD | 26 min read
Solidity BasicsPart 3: Advanced Patterns

Series: Web3 Security Zero se Advance 🛡️ | Article #7 By HackerMD | 26 min read
Aaj Kya Seekhenge?
- Receive & Fallback functions ETH receive karna
- Low-level calls call, delegatecall, staticcall
- Assembly (Yul) inline assembly basics
- ABI Encoding abi.encode, abi.encodePacked
- Contract creation CREATE vs CREATE2
- Gas optimization patterns
- Checks-Effects-Interactions (CEI) pattern
- ReentrancyGuard kaise kaam karta hai
- Har concept pe security bugs aur attacks!
Hacker Note: Yeh article ka har concept directly kisi na kisi million dollar hack se linked hai! Fallback function = TheDAO hack! Low-level calls = Reentrancy! CREATE2 = Salting attacks! Ek ek cheez dhyan se padho!
PART 1: Receive & Fallback ETH Kaise Aata Hai Contract Mein?
// Contract ETH receive karne ke liye
// Special functions chahiye!
contract ETHReceiver {
event Received(address from, uint256 amount);
event FallbackCalled(
address from,
uint256 amount,
bytes data
);
// ─── receive() ────────────────────────
// Jab:
// → Pure ETH transfer aaye (no data)
// → msg.data EMPTY ho
// Conditions:
// → MUST be external
// → MUST be payable
// → No arguments, no return
receive() external payable {
emit Received(msg.sender, msg.value);
// Simple ETH receive!
}
// ─── fallback() ───────────────────────
// Jab:
// → Koi function match nahi karta
// → msg.data non-empty ho
// → receive() nahi hai aur ETH aaye
// Conditions:
// → MUST be external
// → payable optional
fallback() external payable {
emit FallbackCalled(
msg.sender,
msg.value,
msg.data
);
}
}
// ETH receive decision tree:
//
// ETH Transfer aaya
// │
// msg.data empty?
// ┌─────┴──────┐
// YES NO
// │ │
// receive() fallback()
// exist? exist?
// ┌──┴──┐ ┌──┴──┐
// YES NO YES NO
// │ │ │ │
// run fallback run REVERT!
// run?
// ┌┴┐
// YES NO
// │ │
// run REVERT
Fallback Security Bugs:
// BUG 1: TheDAO Style — Fallback Reentrancy!
// (2016 — $60M hack ka origin)
contract VulnerableDAO {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "Nothing to withdraw!");
// ⚠️ External call PEHLE!
(bool ok,) = msg.sender.call{
value: amount
}("");
require(ok, "Transfer failed!");
// ⚠️ State update BAAD MEIN!
balances[msg.sender] = 0;
// Attacker ka fallback/receive baar baar
// withdraw() call karta hai!
// Balance kabhi 0 nahi hota withdraw se pehle!
}
}
// Attacker contract:
contract Attacker {
VulnerableDAO dao;
uint256 public count;
constructor(address _dao) {
dao = VulnerableDAO(_dao);
}
function attack() external payable {
dao.deposit{value: msg.value}();
dao.withdraw();
}
// Yeh baar baar call hoga!
receive() external payable {
count++;
if (count < 10) {
dao.withdraw(); // Re-enter!
}
}
}
// ✅ FIX: CEI Pattern (baad mein detail mein!)
function withdrawSafe() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "Nothing!");
// Effect FIRST!
balances[msg.sender] = 0;
// Interaction LAST!
(bool ok,) = msg.sender.call{
value: amount
}("");
require(ok, "Transfer failed!");
}
// ─────────────────────────────────────────
// BUG 2: Accidental ETH lock!
contract NoReceive {
// receive() aur fallback() DONO missing!
function doSomething() external {
// Normal function
}
// Koi ETH bheje → REVERT!
// ETH transfer always fails!
// Agar protocol ETH expect karta tha:
// → Funds stuck forever!
}
// ─────────────────────────────────────────
// BUG 3: Payable fallback — unintended ETH accept!
contract Dangerous {
// Developer ne sirf fallback likha
// "proxy ke liye"
fallback() external payable {
// Forward to implementation...
}
// Ab koi bhi ETH bhej sakta hai!
// Contract ETH hold karne ke liye
// designed nahi tha!
// ETH permanently stuck!
}
PART 2: Low-Level Calls Powerful aur Dangerous!
contract LowLevelCalls {
// ─── .call() ──────────────────────────
// Sabse flexible!
// ETH transfer + function call
// Returns: (bool success, bytes memory data)
function callExample(
address target,
bytes memory data
) external payable returns (bool, bytes memory) {
// With ETH:
(bool ok, bytes memory ret) =
target.call{
value: msg.value,
gas: 50000 // Gas limit optional
}(data);
return (ok, ret);
}
// Specific function call:
function callTransfer(
address token,
address to,
uint256 amount
) external returns (bool) {
(bool ok, bytes memory data) = token.call(
abi.encodeWithSignature(
"transfer(address,uint256)",
to,
amount
)
);
// Return value decode:
if (ok && data.length > 0) {
return abi.decode(data, (bool));
}
return ok;
}
// ─── .delegatecall() ──────────────────
// Target ka CODE — Caller ka CONTEXT!
// msg.sender = Original caller
// Storage = Calling contract ka!
// ⚠️ EXTREMELY DANGEROUS!
function delegateExample(
address implementation,
bytes memory data
) external returns (bool, bytes memory) {
(bool ok, bytes memory ret) =
implementation.delegatecall(data);
return (ok, ret);
}
// ─── .staticcall() ────────────────────
// Read-only call!
// State change → REVERT!
// Safe for reading data
function staticExample(
address target,
bytes memory data
) external view returns (bytes memory) {
(bool ok, bytes memory ret) =
target.staticcall(data);
require(ok, "Static call failed!");
return ret;
}
}
Call vs DelegateCall vs StaticCall:
Feature | call() | delegatecall() | staticcall()
-----------------|-------------|----------------|-------------
msg.sender | This contract| Original caller| This contract
msg.value | Forwarded | Caller's value | 0
Storage used | Target's | CALLER'S! | Target's
State change | Allowed | CALLER's state!| FORBIDDEN!
ETH transfer | Yes | No | No
Use case | Normal calls| Proxy pattern | Read only
Security risk | Reentrancy | Storage collision| Safe!
DELEGATECALL VISUAL:
┌──────────────────┐ delegatecall ┌──────────────────┐
│ Contract A │ ─────────────────→ │ Library/Impl B │
│ │ │ │
│ Storage: │ │ Code runs here │
│ slot0: owner │ ←── modifies ──── │ but A's storage │
│ slot1: balance │ │ is affected! │
└──────────────────┘ └──────────────────┘
Low-Level Call Bugs:
// BUG 1: Return value not checked!
contract Unchecked {
function sendETH(address to) external {
// ⚠️ Return value IGNORE!
to.call{value: 1 ether}("");
// Agar call fail ho?
// No revert! Code continues!
// ETH lost silently!
}
// ✅ CORRECT:
function sendETHSafe(address to) external {
(bool ok,) = to.call{value: 1 ether}("");
require(ok, "ETH transfer failed!");
}
}
// ─────────────────────────────────────────
// BUG 2: Arbitrary delegatecall!
contract ArbitraryDelegate {
address public owner; // slot 0
// ⚠️ User controlled target!
function execute(
address target, // Attacker controls this!
bytes memory data
) external {
target.delegatecall(data);
// Attacker passes malicious contract:
// contract Evil {
// function pwn() external {
// // slot 0 = owner!
// assembly { sstore(0, caller()) }
// }
// }
// Result: owner = attacker!
}
}
// ─────────────────────────────────────────
// BUG 3: Gas forwarding attack!
contract GasIssue {
function forward(address target) external {
// All gas forward kar raha hai!
target.call{gas: gasleft()}(
abi.encodeWithSignature("expensive()")
);
// Target ko infinite gas!
// Return bomb possible!
}
// ✅ CORRECT: Gas limit lagao!
function forwardSafe(address target) external {
target.call{gas: 50000}(
abi.encodeWithSignature("safe()")
);
}
}
PART 3: Inline Assembly EVM Ka Direct Access!
// Assembly = EVM Opcodes directly Solidity mein
// Use cases:
// → Gas optimization
// → Operations Solidity mein possible nahi
// → Low-level storage manipulation
// → Custom memory management
contract AssemblyExamples {
// ─── Basic Assembly ───────────────────
function addAssembly(uint256 a, uint256 b)
public pure
returns (uint256 result)
{
assembly {
result := add(a, b)
// := assignment in Yul
}
}
// ─── Storage Direct Access ────────────
function setSlot(uint256 slot, uint256 value)
internal
{
assembly {
sstore(slot, value)
}
}
function getSlot(uint256 slot)
internal view
returns (uint256 value)
{
assembly {
value := sload(slot)
}
}
// ─── Address Operations ───────────────
function getContractSize(address addr)
public view
returns (uint256 size)
{
assembly {
size := extcodesize(addr)
}
// extcodesize = 0 → EOA
// extcodesize > 0 → Contract
// ⚠️ But: Constructor ke time = 0!
// isContract() bypass possible!
}
// ─── Efficient ETH Transfer ───────────
function efficientTransfer(
address to,
uint256 amount
) internal {
assembly {
// Direct ETH transfer
let ok := call(
gas(), // All gas
to, // Recipient
amount, // Value
0, 0, // No input data
0, 0 // No output
)
if iszero(ok) {
revert(0, 0)
}
}
}
// ─── Memory Operations ────────────────
function memoryExample()
public pure
returns (bytes32 result)
{
assembly {
// Free memory pointer:
let ptr := mload(0x40)
// 0x40 = Free memory pointer location!
// Store data:
mstore(ptr, 0x12345678)
// Update free memory pointer:
mstore(0x40, add(ptr, 0x20))
result := mload(ptr)
}
}
}
Assembly Security Bugs:
// BUG 1: isContract bypass via constructor!
contract ConstructorBypass {
modifier onlyEOA() {
uint256 size;
assembly {
size := extcodesize(caller())
}
require(size == 0, "No contracts!");
_;
}
function sensitiveFunc()
external
onlyEOA
{
// "Only EOA can call" — OR CAN THEY?
}
}
// Attack:
contract Attacker {
constructor(address target) {
// Constructor ke time:
// extcodesize(this) = 0!
// Even though we ARE a contract!
ConstructorBypass(target).sensitiveFunc();
// ← Check passes! We're a contract!
// but check thinks we're EOA!
}
}
// ─────────────────────────────────────────
// BUG 2: Memory corruption!
contract MemoryBug {
function dangerous(bytes memory data)
external pure
returns (uint256)
{
assembly {
// ⚠️ Free memory pointer nahi update kiya!
let ptr := mload(0x40)
mstore(ptr, mload(add(data, 0x20)))
// 0x40 update bhool gaya!
// Next allocation same ptr use karega!
// Memory overlap = Data corruption!
}
}
}
// ─────────────────────────────────────────
// BUG 3: Arbitrary storage write!
contract StorageWrite {
address public owner; // slot 0
uint256 public totalFunds; // slot 1
function updateConfig(
uint256 slot, // ⚠️ User controlled!
uint256 value
) external {
assembly {
sstore(slot, value)
// Attacker → slot=0, value=attacker_addr
// owner = attacker!
}
}
}
PART 4: ABI Encoding Data Kaise Pack Hota Hai!
contract ABIEncoding {
// ─── abi.encode() ─────────────────────
// Padded encoding — 32 bytes per element
// Safe for all types!
function encodeExample()
external pure
returns (bytes memory)
{
return abi.encode(
uint256(100),
address(0xABC...),
bool(true)
);
// Each value padded to 32 bytes:
// 0x0000...0064 (100)
// 0x0000...ABC (address)
// 0x0000...0001 (true)
// Total: 96 bytes
}
// ─── abi.encodePacked() ───────────────
// Compact encoding — NO padding!
// Gas efficient
// ⚠️ COLLISION POSSIBLE!
function encodePackedExample()
external pure
returns (bytes memory)
{
return abi.encodePacked(
uint256(100),
address(0xABC...)
);
// No padding! Tight packing!
// 32 bytes + 20 bytes = 52 bytes
}
// ─── abi.encodeWithSelector() ─────────
// Function call data encode karo!
function encodeCall()
external pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256(
"transfer(address,uint256)"
)),
address(0xBob),
uint256(1000)
);
// = 0xa9059cbb + encoded args
}
// ─── abi.encodeWithSignature() ────────
// String signature use karo
function encodeWithSig()
external pure
returns (bytes memory)
{
return abi.encodeWithSignature(
"transfer(address,uint256)",
address(0xBob),
uint256(1000)
);
// Same as above — cleaner syntax!
}
// ─── Decoding ─────────────────────────
function decodeExample(bytes memory data)
external pure
returns (uint256, address, bool)
{
return abi.decode(
data,
(uint256, address, bool)
);
}
}
ABI Encoding Bugs:
// BUG: encodePacked Collision Attack!
// Hash signing mein bahut common!
contract VulnerableSig {
function getHash(
address user,
uint256 amount,
string memory message
) public pure returns (bytes32) {
// ⚠️ encodePacked collision!
return keccak256(
abi.encodePacked(user, amount, message)
);
}
}
// ATTACK:
// Normal call:
// user=0xAlice, amount=100, message="hello"
// Packed: [alice_20bytes][100_32bytes][hello_5bytes]
// Attacker:
// user=0xAlice, amount=100, message="hell"
// + different amount in message!
// Packed bytes can be SAME!
// Hash collision! Signature reuse!
// ✅ FIX: Use abi.encode() for dynamic types!
function getHashSafe(
address user,
uint256 amount,
string memory message
) public pure returns (bytes32) {
return keccak256(
abi.encode(user, amount, message)
// Padded encoding = No collision!
);
}
// ─────────────────────────────────────────
// REAL EXAMPLE: Signature Replay
contract VulnPayment {
mapping(bytes32 => bool) public usedHashes;
function pay(
address to,
uint256 amount,
uint8 v, bytes32 r, bytes32 s
) external {
bytes32 hash = keccak256(
abi.encodePacked(to, amount)
// ⚠️ No chainId! No nonce!
);
address signer = ecrecover(hash, v, r, s);
require(signer == owner, "Invalid sig!");
// ⚠️ No hash usage tracking!
payable(to).transfer(amount);
// Replay attack!
// Same signature → Infinite payments!
}
// ✅ CORRECT:
function paySafe(
address to,
uint256 amount,
uint256 nonce,
uint8 v, bytes32 r, bytes32 s
) external {
bytes32 hash = keccak256(
abi.encode(
to,
amount,
nonce,
block.chainid, // Chain specific!
address(this) // Contract specific!
)
);
require(!usedHashes[hash], "Replayed!");
usedHashes[hash] = true; // Mark used!
address signer = ecrecover(hash, v, r, s);
require(signer == owner, "Invalid!");
payable(to).transfer(amount);
}
}
PART 5: Contract Creation CREATE vs CREATE2!
// ─── CREATE (Regular) ─────────────────────
// Address = keccak256(deployer_address + nonce)
// Nonce se predictable — lekin pehle se predict karna mushkil
contract Factory {
event ContractCreated(address newContract);
function createChild() external
returns (address)
{
// Regular CREATE:
ChildContract child = new ChildContract(
msg.sender
);
emit ContractCreated(address(child));
return address(child);
}
}
// ─────────────────────────────────────────
// ─── CREATE2 (Deterministic) ──────────────
// Address = keccak256(0xFF + deployer + salt + bytecodeHash)
// SALT control karo → Address predict karo!
// Deploy karo → Destroy karo → Same address pe re-deploy!
contract Factory2 {
event ContractCreated(
address newContract,
bytes32 salt
);
function createWithSalt(
bytes32 salt
) external returns (address) {
ChildContract child = new ChildContract{
salt: salt
}(msg.sender);
emit ContractCreated(address(child), salt);
return address(child);
}
// Address predict karo DEPLOY se pehle!
function predictAddress(
bytes32 salt
) external view returns (address) {
bytes memory bytecode = abi.encodePacked(
type(ChildContract).creationCode,
abi.encode(msg.sender)
);
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xFF),
address(this),
salt,
keccak256(bytecode)
)
);
return address(uint160(uint256(hash)));
}
}
contract ChildContract {
address public owner;
constructor(address _owner) {
owner = _owner;
}
}
CREATE2 Security Bugs:
// BUG: Metamorphic Contract Attack!
// Same address pe different code deploy!
// Step 1: Malicious factory deploy karo
contract MetamorphicFactory {
mapping(bytes32 => address) public contracts;
function deploy(
bytes32 salt,
bytes memory initCode
) external returns (address addr) {
assembly {
addr := create2(
0,
add(initCode, 0x20),
mload(initCode),
salt
)
}
}
function destroy(bytes32 salt) external {
address target = contracts[salt];
// Selfdestruct the deployed contract
IDestructible(target).destroy();
delete contracts[salt];
}
}
// ATTACK SCENARIO:
// 1. Audit request: Deploy "safe" contract at addr X
// Auditors verify code at X → Clean! ✅
//
// 2. SELFDESTRUCT the contract at X!
// (Same address pe code wipe!)
//
// 3. CREATE2 same salt → Same address X!
// Deploy MALICIOUS code at same X!
//
// 4. Users trust "audited" address X
// Reality: Malicious code!
//
// Defense:
// → SELFDESTRUCT wale contracts trust mat karo!
// → Code hash verify karo har interaction pe!
// → Immutable contracts prefer karo!
PART 6: CEI Pattern Sabse Important Security Pattern!
// CEI = Checks → Effects → Interactions
//
// Checks = All validations first
// Effects = State changes second
// Interactions = External calls LAST!
//
// Kyun? Reentrancy prevent karne ke liye!
// ⚠️ WRONG ORDER (Vulnerable):
contract WrongOrder {
mapping(address => uint256) balances;
function withdraw(uint256 amount) external {
// Check ✅
require(balances[msg.sender] >= amount);
// Interaction ← WRONG POSITION!
(bool ok,) = msg.sender.call{
value: amount
}("");
require(ok);
// Effect ← TOO LATE!
balances[msg.sender] -= amount;
// Attacker re-entered before this!
// Balance never decremented in time!
}
}
// ✅ CORRECT ORDER (CEI):
contract CorrectCEI {
mapping(address => uint256) balances;
function withdraw(uint256 amount) external {
// ─── CHECKS ───────────────────────
require(amount > 0, "Zero amount!");
require(
balances[msg.sender] >= amount,
"Insufficient!"
);
// ─── EFFECTS ──────────────────────
// State update PEHLE!
balances[msg.sender] -= amount;
// Ab re-entry karo toh:
// balance = 0 → require fail!
// ─── INTERACTIONS ─────────────────
// External call BAAD MEIN!
(bool ok,) = msg.sender.call{
value: amount
}("");
require(ok, "Transfer failed!");
}
}
PART 7: ReentrancyGuard Defense in Depth!
// ReentrancyGuard = Mutex lock for contracts
// Ek function execute ho raha hai →
// Same function dobara call → REVERT!
// OpenZeppelin implementation:
abstract contract ReentrancyGuard {
// Status values:
uint256 private constant NOT_ENTERED = 1;
uint256 private constant ENTERED = 2;
uint256 private _status;
constructor() {
_status = NOT_ENTERED;
}
modifier nonReentrant() {
// Check: Already entered?
require(
_status != ENTERED,
"ReentrancyGuard: reentrant call"
);
// Set: Entered!
_status = ENTERED;
_; // Function body
// Reset: Not entered!
_status = NOT_ENTERED;
}
}
// Usage:
contract SecureVault is ReentrancyGuard {
mapping(address => uint256) balances;
function withdraw(uint256 amount)
external
nonReentrant // ← One call at a time!
{
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
(bool ok,) = msg.sender.call{
value: amount
}("");
require(ok);
}
}
// ⚠️ ReentrancyGuard bypass:
// Cross-function reentrancy!
contract CrossReentrancy is ReentrancyGuard {
mapping(address => uint256) balances;
mapping(address => bool) public locked;
// nonReentrant protect karta hai
// Same function ko!
// Lekin DIFFERENT function?
function withdrawA(uint256 amount)
external
nonReentrant
{
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
// External call:
msg.sender.call{value: amount}("");
// Attacker re-enters withdrawB!
}
function withdrawB(uint256 amount)
external
nonReentrant
// ← Different nonReentrant status!
// withdrawA ka ENTERED alag hai!
// withdrawB ka apna fresh status hai!
{
// ⚠️ Balance already decremented in A
// But if logic here is different...
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
// Double withdrawal possible in some cases!
}
}
// ✅ Complete Defense:
// 1. CEI pattern use karo
// 2. nonReentrant lagao
// 3. Cross-function bhi consider karo!
PART 8: Gas Optimization Patterns!
contract GasOptimization {
// ─── Tip 1: uint256 > uint8 ───────────
// Counterintuitive! uint8 MORE gas in some cases!
uint8 public small; // More gas! (padding needed)
uint256 public large; // Less gas in isolation!
// ─── Tip 2: Storage Packing ───────────
// WRONG: 3 slots!
uint256 a; // slot 0
uint8 b; // slot 1 (wasted space!)
uint256 c; // slot 2
// CORRECT: 2 slots!
uint256 d; // slot 0
uint8 e; // slot 1 (packed!)
uint8 f; // slot 1 (same slot!)
uint8 g; // slot 1 (same slot!)
uint256 h; // slot 2
// ─── Tip 3: Memory vs Storage ─────────
function expensive(uint256[] storage arr)
internal view
returns (uint256 sum)
{
for (uint i = 0; i < arr.length; i++) {
sum += arr[i]; // SLOAD every time!
}
}
function cheap(uint256[] memory arr)
internal pure
returns (uint256 sum)
{
for (uint i = 0; i < arr.length; i++) {
sum += arr[i]; // MLOAD — cheaper!
}
}
// ─── Tip 4: Cache Array Length ────────
// WRONG:
function wrongLoop(uint256[] memory arr)
external pure
{
for (uint i = 0; i < arr.length; i++) {
// arr.length har iteration compute!
}
}
// CORRECT:
function rightLoop(uint256[] memory arr)
external pure
{
uint256 len = arr.length; // Cache once!
for (uint i = 0; i < len; i++) {
// len = just a variable read
}
}
// ─── Tip 5: Immutable vs Constant ─────
address public constant CONST_ADDR =
address(0x123); // Compile-time replacement
address public immutable IMMUT_ADDR;
// Deploy-time — cheaper than storage!
// ─── Tip 6: Custom Errors > Strings ───
// WRONG: String error message
function wrongError() external pure {
require(false, "This is a long error!");
// String stored in bytecode = Size + Gas!
}
// CORRECT: Custom error
error MyError();
function rightError() external pure {
revert MyError();
// Just 4 bytes selector!
}
// ─── Tip 7: Events > Storage ──────────
// Historical data ke liye events use karo
// Events = Logs = Cheaper than storage!
// Storage: 20,000 gas (new slot)
// Event: 375+ gas
// ─── Tip 8: Short-circuit evaluation ──
function shortCircuit(
uint256 x,
address addr
) external view returns (bool) {
// Cheap check PEHLE!
return x > 0 &&
addr != address(0) &&
addr.code.length == 0;
// Expensive check BAAD MEIN!
}
}
PART 9: Complete Pattern Production Ready Contract!
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/// @title AdvancedVault
/// @notice All advanced patterns applied!
/// @dev CEI + ReentrancyGuard + Assembly optimize
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract AdvancedVault is
ReentrancyGuard,
Ownable
{
// ─── Storage (Optimized Packing) ──────
address public immutable token; // 20 bytes
bool public paused; // 1 byte ↕ Packed!
uint96 public totalFees; // 12 bytes ↕ Packed!
// Slot 0: token(20) + paused(1) + totalFees(12) = 33? No!
// Actually: address = 20 bytes, next slot
uint256 public totalDeposited;
uint256 public feeRate; // basis points (100 = 1%)
mapping(address => uint256) private _balances;
mapping(address => uint256) private _lastAction;
// ─── Events ───────────────────────────
event Deposited(
address indexed user,
uint256 amount,
uint256 fee
);
event Withdrawn(
address indexed user,
uint256 amount
);
// ─── Custom Errors ────────────────────
error Paused();
error ZeroAmount();
error ZeroAddress();
error InsufficientBalance(
uint256 have,
uint256 need
);
error CooldownActive(uint256 remaining);
error InvalidFeeRate(uint256 rate);
// ─── Constants ────────────────────────
uint256 public constant MAX_FEE_RATE = 1000; // 10%
uint256 public constant COOLDOWN = 1 minutes;
uint256 private constant FEE_DENOMINATOR = 10000;
// ─── Modifiers ────────────────────────
modifier whenActive() {
if (paused) revert Paused();
_;
}
modifier cooldownPassed() {
uint256 lastAction = _lastAction[msg.sender];
if (lastAction != 0) {
uint256 elapsed = block.timestamp - lastAction;
if (elapsed < COOLDOWN) {
revert CooldownActive(COOLDOWN - elapsed);
}
}
_;
}
// ─── Constructor ──────────────────────
constructor(
address _token,
uint256 _feeRate
) Ownable(msg.sender) {
if (_token == address(0)) revert ZeroAddress();
if (_feeRate > MAX_FEE_RATE)
revert InvalidFeeRate(_feeRate);
token = _token;
feeRate = _feeRate;
}
// ─── Core Functions ───────────────────
function deposit(uint256 amount)
external
whenActive
nonReentrant
cooldownPassed
{
// ─── CHECKS ───────────────────────
if (amount == 0) revert ZeroAmount();
// ─── EFFECTS ──────────────────────
uint256 fee = (amount * feeRate) /
FEE_DENOMINATOR;
uint256 netAmount = amount - fee;
_balances[msg.sender] += netAmount;
totalDeposited += netAmount;
totalFees += uint96(fee);
_lastAction[msg.sender] = block.timestamp;
// ─── INTERACTIONS ─────────────────
_safeTransferFrom(
token,
msg.sender,
address(this),
amount
);
emit Deposited(msg.sender, netAmount, fee);
}
function withdraw(uint256 amount)
external
whenActive
nonReentrant
{
// ─── CHECKS ───────────────────────
if (amount == 0) revert ZeroAmount();
uint256 bal = _balances[msg.sender];
if (bal < amount)
revert InsufficientBalance(bal, amount);
// ─── EFFECTS ──────────────────────
_balances[msg.sender] -= amount;
totalDeposited -= amount;
// ─── INTERACTIONS ─────────────────
_safeTransfer(token, msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
// ─── Internal Helpers ─────────────────
function _safeTransfer(
address _token,
address to,
uint256 amount
) internal {
(bool ok, bytes memory data) =
_token.call(
abi.encodeWithSignature(
"transfer(address,uint256)",
to,
amount
)
);
require(
ok && (data.length == 0 ||
abi.decode(data, (bool))),
"Transfer failed!"
);
}
function _safeTransferFrom(
address _token,
address from,
address to,
uint256 amount
) internal {
(bool ok, bytes memory data) =
_token.call(
abi.encodeWithSignature(
"transferFrom(address,address,uint256)",
from,
to,
amount
)
);
require(
ok && (data.length == 0 ||
abi.decode(data, (bool))),
"TransferFrom failed!"
);
}
// ─── View Functions ───────────────────
function balanceOf(address user)
external view returns (uint256)
{
return _balances[user];
}
// ─── Admin ────────────────────────────
function setPause(bool _paused)
external onlyOwner
{
paused = _paused;
}
function setFeeRate(uint256 _feeRate)
external onlyOwner
{
if (_feeRate > MAX_FEE_RATE)
revert InvalidFeeRate(_feeRate);
feeRate = _feeRate;
}
}
PART 10: Final Bug Hunt Challenge!
// ─── 6 bugs dhundho! ──────────────────────
contract AdvancedBuggy {
mapping(address => uint256) balances;
address public implementation;
bool private locked;
// Bug #1:
receive() external {
balances[msg.sender] += msg.value;
}
// Bug #2:
function delegateTo(bytes memory data)
external
{
implementation.delegatecall(data);
}
// Bug #3:
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
(bool ok,) = msg.sender.call{
value: amount
}("");
require(ok);
balances[msg.sender] -= amount;
}
// Bug #4:
function getHash(
address user,
uint256 amount
) public pure returns (bytes32) {
return keccak256(
abi.encodePacked(user, amount)
);
}
// Bug #5:
modifier noReentrant() {
require(!locked, "Locked!");
locked = true;
_;
}
// Bug #6:
function create2Deploy(
bytes32 salt,
bytes memory code
) external returns (address addr) {
assembly {
addr := create2(
0,
add(code, 0x20),
mload(code),
salt
)
}
}
}
// ─── ANSWERS ──────────────────────────────
// Bug #1: receive() not payable!
// ETH receive karna hai → payable zaroori!
// receive() external payable { ... }
// Bug #2: Arbitrary delegatecall!
// Koi bhi implementation set kar sakta hai
// + Koi bhi data pass kar sakta hai
// = Storage corruption + owner takeover!
// Fix: onlyOwner + whitelist allowed calls!
// Bug #3: CEI violation!
// External call pehle → State update baad
// Reentrancy attack possible!
// Fix: balances -= amount PEHLE!
// Bug #4: encodePacked collision!
// Dynamic types ke saath collision possible
// abi.encode() use karo!
// Plus: No chainId, no nonce = Replay attack!
// Bug #5: nonReentrant lock reset missing!
// locked = true set kiya
// Lekin function ke baad locked = false
// KABHI NAHI HOGA!
// Contract permanently locked after first call!
// Fix: locked = false; after _;
// Bug #6: create2Deploy access control missing!
// Koi bhi arbitrary code deploy kar sakta hai
// Factory contract ke naam pe!
// Trust issue + Potential attack surface
// Fix: onlyOwner modifier!
Quick Revision
📥 receive() = Pure ETH receive (no data)
MUST be payable!
📥 fallback() = Fallback (data ya no receive)
Reentrancy entry point! ⚠️
📞 .call() = ETH + function, return check karo!
📞 .delegatecall= Code borrow, caller storage ⚠️
📞 .staticcall() = Read only, safe!
⚙️ Assembly = Direct EVM access
isContract bypass possible! ⚠️
Storage arbitrary write! ⚠️
🔡 abi.encode = Padded, safe, collision-free ✅
🔡 encodePacked = Compact, collision possible ⚠️
Dynamic types ke saath dangerous!
🏭 CREATE = Nonce-based address
🏭 CREATE2 = Salt-based deterministic address
Metamorphic contract attack! ⚠️
🛡️ CEI Pattern = Checks → Effects → Interactions
MOST IMPORTANT security pattern!
🔒 ReentrancyGuard= Mutex lock
Cross-function bypass possible! ⚠️
⛽ Gas Tips:
Storage pack karo
Custom errors use karo
Array length cache karo
Immutable/Constant prefer karo
Meri Baat…
Teen articles mein Solidity cover ki। Ab tum jaante ho:
Article #5: Variables, Functions, Types
Article #6: OOP, Inheritance, Interfaces
Article #7: Advanced Patterns, Assembly, CEI
Yeh sab combine karo:
Ek Solidity contract open karo →
→ Variables dekho (overflow possible?)
→ Functions dekho (modifier missing?)
→ Inheritance chain dekho (bypass?)
→ receive/fallback dekho (reentrancy?)
→ Low-level calls dekho (CEI?)
→ Assembly dekho (storage write?)
→ ABI encoding dekho (collision?)
Yeh hai ek BASIC audit checklist!
Real auditors yahi karte hain —
Sirf zyada systematically!
Tumne 7 articles mein woh seekha
jo bahut log 6 months mein seekhte hain!
Agle phase: TOOLS!
Foundry se exploit likhenge —
Real contracts pe test karenge —
Automated bugs dhundhenge!
Taiyar ho? 🚀
Article #8 mein: Smart Contract Lifecycle Deploy se Exploit tak poora journey! ⚡
HackerMD Web3 Security Researcher GitHub: BotGJ16 | Medium: @HackerMD
Previous: Article #6 Solidity OOP & Inheritance Next: Article #8 Smart Contract Lifecycle
#Solidity #AdvancedPatterns #CEI #Reentrancy #Web3Security #BugBounty #Hinglish #HackerMD
메타데이터
- post_id
- 9f9e8a946baa
- slug
- solidity-basicspart-3-advanced-patterns-9f9e8a946baa
- url
- https://medium.com/@HackerMD/solidity-basicspart-3-advanced-patterns-9f9e8a946baa
- canonical_url
- https://medium.com/@HackerMD/solidity-basicspart-3-advanced-patterns-9f9e8a946baa
- author_url
- https://medium.com/@HackerMD
- status
- ok
- fetched_at
- 2026-06-16 19:09:56