Security·8 min का पठन·Solingo द्वारा

Reentrancy Attack की पूरी जानकारी — यह कैसे काम करता है और कैसे रोकें

Reentrancy vulnerability को समझें जिसने The DAO hack को जन्म दिया और सीखें proven techniques जो आपके smart contracts को इस critical attack vector से बचा सकती हैं।

# Reentrancy Attack की पूरी जानकारी — यह कैसे काम करता है और कैसे रोकें

Reentrancy, smart contract security में सबसे खतरनाक vulnerabilities में से एक है। 2016 का infamous DAO hack, जिसने $60 million से ज़्यादा Ether को drain कर दिया था, एक reentrancy attack की वजह से हुआ था। Well-known होने के बावजूद, reentrancy vulnerabilities आज भी modern contracts में दिखाई देती हैं।

इस article में, हम explore करेंगे कि reentrancy attacks कैसे काम करते हैं, different types of reentrancy को examine करेंगे, और proven prevention techniques सीखेंगे।

Reentrancy Attack क्या है?

Reentrancy attack तब होता है जब state को update करने से पहले external contract call किया जाता है, जिससे called contract original function में re-enter कर सके और inconsistent state को exploit कर सके।

Classic Example: Vulnerable Withdrawal

contract VulnerableBank {

mapping(address => uint256) public balances;

function deposit() external payable {

balances[msg.sender] += msg.value;

}

// VULNERABLE: External call before state update

function withdraw(uint256 amount) external {

require(balances[msg.sender] >= amount, "Insufficient balance");

// External call BEFORE state update

(bool success, ) = msg.sender.call{value: amount}("");

require(success, "Transfer failed");

// State update AFTER external call (TOO LATE!)

balances[msg.sender] -= amount;

}

}

Attack Contract

contract Attacker {

VulnerableBank public bank;

uint256 public constant AMOUNT = 1 ether;

constructor(address _bank) {

bank = VulnerableBank(_bank);

}

// Attack entry point

function attack() external payable {

require(msg.value >= AMOUNT, "Need 1 ETH to attack");

bank.deposit{value: AMOUNT}();

bank.withdraw(AMOUNT);

}

// Re-enter on receive

receive() external payable {

if (address(bank).balance >= AMOUNT) {

bank.withdraw(AMOUNT);

}

}

}

क्या होता है:

  • Attacker attack() को 1 ETH के साथ call करता है
  • Contract 1 ETH deposit करता है (balance = 1)
  • Contract withdraw को 1 ETH के लिए call करता है
  • Bank transfer करता है → attacker का receive() trigger होता है
  • receive() फिर से withdraw() को call करता है (balance अभी भी 1 है!)
  • Bank फिर से transfer करता है → फिर से receive() trigger होता है
  • यह तब तक repeat होता है जब तक bank खाली नहीं हो जाता
  • Balance केवल first call के बाद ही update होता है, लेकिन tab tak contract drain हो चुका होता है।

    Reentrancy के Types

    1. Single-Function Reentrancy

    सबसे common type — एक ही function को repeatedly call करना।

    // VULNERABLE
    

    function withdraw() external {

    uint256 amount = balances[msg.sender];

    (bool success, ) = msg.sender.call{value: amount}("");

    balances[msg.sender] = 0;

    }

    2. Cross-Function Reentrancy

    एक function से दूसरे function में re-enter करना जो same state को modify करता है।

    contract CrossFunctionReentrancy {
    

    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) external {

    require(balances[msg.sender] >= amount);

    balances[msg.sender] -= amount;

    // External call before completing transfer

    (bool success, ) = to.call("");

    require(success);

    balances[to] += amount;

    }

    function withdraw() external {

    uint256 amount = balances[msg.sender];

    // Attacker calls withdraw() during transfer's external call

    (bool success, ) = msg.sender.call{value: amount}("");

    balances[msg.sender] = 0;

    }

    }

    Attacker transfer() call करता है, external call के दौरान withdraw() को call करता है, और balance को दो बार drain कर देता है।

    3. Read-Only Reentrancy

    Attacker state को modify नहीं करता, लेकिन inconsistent state को read करता है।

    contract Vault {
    

    mapping(address => uint256) public shares;

    uint256 public totalShares;

    function withdraw() external {

    uint256 amount = shares[msg.sender];

    shares[msg.sender] = 0;

    (bool success, ) = msg.sender.call{value: amount}("");

    totalShares -= amount; // Updated AFTER call

    }

    function getShareValue() external view returns (uint256) {

    return address(this).balance / totalShares;

    }

    }

    External protocol जो getShareValue() rely करती है, वह incorrect value read करेगी क्योंकि totalShares अभी तक updated नहीं हुआ है।

    Prevention Techniques

    1. Checks-Effects-Interactions Pattern

    Golden Rule: State updates को external calls से पहले करें।

    function withdraw(uint256 amount) external {
    

    // CHECKS

    require(balances[msg.sender] >= amount, "Insufficient balance");

    // EFFECTS (state changes FIRST)

    balances[msg.sender] -= amount;

    // INTERACTIONS (external calls LAST)

    (bool success, ) = msg.sender.call{value: amount}("");

    require(success, "Transfer failed");

    }

    2. ReentrancyGuard Modifier

    OpenZeppelin का ReentrancyGuard lock pattern implement करता है।

    import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
    
    

    contract SecureBank is ReentrancyGuard {

    mapping(address => uint256) public balances;

    function withdraw(uint256 amount) external nonReentrant {

    require(balances[msg.sender] >= amount);

    balances[msg.sender] -= amount;

    (bool success, ) = msg.sender.call{value: amount}("");

    require(success);

    }

    }

    ReentrancyGuard implementation:

    abstract contract ReentrancyGuard {
    

    uint256 private constant _NOT_ENTERED = 1;

    uint256 private constant _ENTERED = 2;

    uint256 private _status;

    constructor() {

    _status = _NOT_ENTERED;

    }

    modifier nonReentrant() {

    require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

    _status = _ENTERED;

    _;

    _status = _NOT_ENTERED;

    }

    }

    3. Pull Payment Pattern

    External transfers को avoid करें — users को खुद funds withdraw करने दें।

    contract PullPayment {
    

    mapping(address => uint256) public pendingWithdrawals;

    function asyncSend(address recipient, uint256 amount) internal {

    pendingWithdrawals[recipient] += amount;

    }

    function withdraw() external {

    uint256 amount = pendingWithdrawals[msg.sender];

    require(amount > 0, "No funds");

    pendingWithdrawals[msg.sender] = 0;

    (bool success, ) = msg.sender.call{value: amount}("");

    require(success);

    }

    }

    4. Use .transfer() या .send() (Limited Approach)

    ये methods केवल 2300 gas forward करते हैं, जो re-entrance को रोकने के लिए काफी नहीं है।

    // Less flexible but safer for simple transfers
    

    payable(msg.sender).transfer(amount);

    ⚠️ Warning: EIP-1884 के बाद, gas costs बदल सकती हैं, इसलिए .transfer() हमेशा safe नहीं है। ReentrancyGuard + .call{} prefer करें।

    Cross-Function Reentrancy से Protection

    nonReentrant modifier को सभी state-modifying functions पर apply करें जो same state को share करते हैं।

    contract SecureVault is ReentrancyGuard {
    

    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) external nonReentrant {

    require(balances[msg.sender] >= amount);

    balances[msg.sender] -= amount;

    balances[to] += amount;

    (bool success, ) = to.call("");

    require(success);

    }

    function withdraw() external nonReentrant {

    uint256 amount = balances[msg.sender];

    balances[msg.sender] = 0;

    (bool success, ) = msg.sender.call{value: amount}("");

    require(success);

    }

    }

    Read-Only Reentrancy से Protection

    View functions में भी nonReentrant use करें जो critical data return करते हैं।

    function getShareValue() external view nonReentrant returns (uint256) {
    

    return address(this).balance / totalShares;

    }

    या: State को externally visible बनाने से पहले completely update करें।

    function withdraw() external {
    

    uint256 amount = shares[msg.sender];

    shares[msg.sender] = 0;

    totalShares -= amount; // Update BEFORE external call

    (bool success, ) = msg.sender.call{value: amount}("");

    require(success);

    }

    Real-World Example: Curve Finance Vyper Reentrancy (2023)

    July 2023 में, Curve Finance pools में एक reentrancy vulnerability discover हुई जो Vyper compiler bug की वजह से थी। nonReentrant decorator properly compile नहीं हो रहा था।

    Impact: $70 million compromised (कुछ funds recover किए गए)

    Lesson: सिर्फ अपने code पर depend न करें — compiler bugs भी risk हैं। हमेशा:

    • Latest stable compiler versions use करें
    • Multiple audits कराएं
    • External security tools (Slither, Mythril) run करें
    • Formal verification consider करें

    Testing for Reentrancy

    Foundry Test Example

    // SPDX-License-Identifier: MIT
    

    pragma solidity ^0.8.0;

    import "forge-std/Test.sol";

    contract ReentrancyTest is Test {

    VulnerableBank bank;

    Attacker attacker;

    function setUp() public {

    bank = new VulnerableBank();

    attacker = new Attacker(address(bank));

    // Fund the bank

    vm.deal(address(bank), 10 ether);

    bank.deposit{value: 5 ether}();

    }

    function testReentrancyAttack() public {

    uint256 bankBalanceBefore = address(bank).balance;

    uint256 attackerBalanceBefore = address(attacker).balance;

    // Execute attack

    vm.deal(address(attacker), 1 ether);

    attacker.attack{value: 1 ether}();

    uint256 bankBalanceAfter = address(bank).balance;

    uint256 attackerBalanceAfter = address(attacker).balance;

    // Assert bank was drained

    assertLt(bankBalanceAfter, bankBalanceBefore);

    assertGt(attackerBalanceAfter, attackerBalanceBefore);

    }

    }

    Detection Tools

    Slither — static analysis tool:

    slither . --detect reentrancy-eth

    Mythril — symbolic execution:

    myth analyze contracts/MyContract.sol

    Echidna — fuzzing tool जो reentrancy paths discover करने की कोशिश करती है।

    Best Practices Checklist

    • ✅ हमेशा Checks-Effects-Interactions pattern follow करें
    • ✅ सभी state-changing functions पर nonReentrant modifier use करें
    • ✅ Pull payment pattern को push के बजाय prefer करें
    • ✅ External calls से पहले state को पूरी तरह update करें
    • ✅ Multiple audits कराएं
    • ✅ Slither/Mythril से automated testing करें
    • ✅ Unit tests में reentrancy scenarios को explicitly test करें
    • ✅ Cross-contract interactions को carefully review करें

    Conclusion

    Reentrancy आज भी DeFi में सबसे devastating attack vectors में से एक है। The DAO hack से लेकर recent Curve incident तक, यह vulnerability billions of dollars को risk में डालती रही है।

    Key takeaways:

    • State updates हमेशा external calls से पहले करें
    • ReentrancyGuard को default security layer के रूप में use करें
    • सभी state-sharing functions को protect करें
    • View functions को भी रक्षा की जरूरत होती है (read-only reentrancy)
    • Code पर भरोसा करें लेकिन verify करें — audits और testing tools अनिवार्य हैं

    Solingo पर, हम आपको real-world scenarios के साथ reentrancy और अन्य security vulnerabilities को पहचानना और रोकना सिखाते हैं। Interactive challenges के साथ practice करें और production-grade smart contract security master करें।

    अभी शुरू करें: solingo.io/challenges/reentrancy

    Practice में लगाने के लिए तैयार हैं?

    Solingo पर interactive exercises के साथ इन concepts को apply करें।

    मुफ्त में शुरू करें