# 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);
}
}
}
क्या होता है:
attack() को 1 ETH के साथ call करता हैreceive() trigger होता हैreceive() फिर से withdraw() को call करता है (balance अभी भी 1 है!)receive() trigger होता है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 पर
nonReentrantmodifier 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