π‘ Introduction
Welcome to Day 20 of #30DaysOfSolidity!
Today, weβre tackling one of the most critical smart contract security topics β Reentrancy Attacks π
Weβll build a Secure Digital Vault, where users can deposit and withdraw tokenized gold (or any ERC-20 asset) safely β just like a decentralized version of Fort Knox.
This project demonstrates how a simple withdrawal function can become a security vulnerability, and how to fix it using:
- The
nonReentrant
modifier - The Checks-Effects-Interactions (CEI) pattern
- OpenZeppelinβs SafeERC20 for safe token transfers
π§ What Is a Reentrancy Attack?
A reentrancy attack occurs when a malicious contract repeatedly calls a vulnerable function before the previous execution finishes.
If the contract updates user balances after sending tokens, attackers can exploit that gap to withdraw multiple times before the balance changes β draining all funds. π±
π‘οΈ Our Defense: The FortKnox Pattern
To make our vault impenetrable, we use two layers of protection:
π§± 1. nonReentrant
Modifier
A mutex (lock) prevents re-entry into functions already being executed.
If one transaction is in progress, no nested calls can occur until it finishes.
βοΈ 2. Checks-Effects-Interactions (CEI) Pattern
Always follow this safe order:
- β Check: Validate conditions
- βοΈ Effect: Update state (like user balance)
- π Interact: Finally, transfer tokens or call external contracts
This pattern minimizes risk even if nonReentrant
were misused.
π§© File Structure
day-20-fortknox/
βββ contracts/
β βββ FortKnoxVault.sol
βββ test/
β βββ FortKnoxVault.t.sol
βββ scripts/
β βββ deploy.js
βββ foundry.toml
βββ README.md
πΎ Source Code β FortKnoxVault.sol
Hereβs the complete smart contract, written in Solidity 0.8.19, using OpenZeppelin for safety and ownership control.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/// @title FortKnoxVault β A secure digital vault for tokenized gold
/// @notice Protects against reentrancy using a nonReentrant modifier
contract FortKnoxVault is Ownable {
using SafeERC20 for IERC20;
IERC20 public immutable token;
mapping(address => uint256) private balances;
uint8 private locked;
bool public paused;
event Deposited(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
event Paused(address indexed account);
event Unpaused(address indexed account);
constructor(IERC20 _token) {
token = _token;
locked = 0;
paused = false;
}
/// @dev Prevents reentrancy (custom mutex pattern)
modifier nonReentrant() {
require(locked == 0, "FortKnox: reentrant call");
locked = 1;
_;
locked = 0;
}
modifier whenNotPaused() {
require(!paused, "FortKnox: paused");
_;
}
/// @notice Deposit ERC20 tokens into the vault
function deposit(uint256 amount) external whenNotPaused {
require(amount > 0, "FortKnox: zero deposit");
balances[msg.sender] += amount;
// Pull tokens from user
token.safeTransferFrom(msg.sender, address(this), amount);
emit Deposited(msg.sender, amount);
}
/// @notice Withdraw your tokens safely (reentrancy protected)
function withdraw(uint256 amount) external nonReentrant whenNotPaused {
require(amount > 0, "FortKnox: zero withdraw");
uint256 bal = balances[msg.sender];
require(bal >= amount, "FortKnox: insufficient balance");
// Effects before interactions
balances[msg.sender] = bal - amount;
// Safe external call
token.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
function balanceOf(address user) external view returns (uint256) {
return balances[user];
}
// Admin controls
function pause() external onlyOwner {
paused = true;
emit Paused(msg.sender);
}
function unpause() external onlyOwner {
paused = false;
emit Unpaused(msg.sender);
}
/// @notice Rescue stuck tokens (only owner)
function rescueTokens(address to, uint256 amount) external onlyOwner {
require(to != address(0), "FortKnox: zero address");
token.safeTransfer(to, amount);
}
}
π§ͺ Testing Scenarios
To ensure full security, test the following cases:
Test Case | Expected Behavior |
---|---|
β Deposit and withdraw | Works normally |
π« Withdraw twice (reentrancy attempt) | Reverts immediately |
π Vault paused | Deposit/withdraw disabled |
β οΈ Withdraw more than balance | Reverts safely |
π§° Admin rescue | Only owner can execute |
You can use Foundry, Hardhat, or Remix for testing.
In Foundry, try simulating a malicious attacker contract calling withdraw()
recursively β it will fail due to the nonReentrant
lock π.
π§© Front-End Demo (Ethers.js + React)
Hereβs a simple snippet to interact with the vault using Ethers.js:
import { ethers } from "ethers";
import FortKnoxVaultABI from "./FortKnoxVault.json";
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const vault = new ethers.Contract(vaultAddress, FortKnoxVaultABI, signer);
// Deposit tokenized gold
const deposit = async (amount) => {
const token = new ethers.Contract(tokenAddress, tokenABI, signer);
await token.approve(vaultAddress, amount);
await vault.deposit(amount);
};
// Withdraw safely
const withdraw = async (amount) => {
await vault.withdraw(amount);
};
This front-end connects MetaMask, allows token deposits, and ensures only the rightful user can withdraw their funds β all secured from reentrancy attacks.
π Security Practices Used
-
nonReentrant
β Prevents nested calls. - Checks-Effects-Interactions β Updates state before transfers.
- SafeERC20 β Handles non-standard tokens safely.
- Pause mechanism β Allows emergency stop.
- Owner-only rescue β Protects from misconfigurations.
- No loops β Avoids gas griefing & DoS risks.
- Event logging β For transparency and audits.
π§ Real-World Analogy
Think of this as a digital Fort Knox π°
- Everyone can deposit their gold (tokens).
- Withdrawals are strictly controlled.
- No one can open the vault door twice at once β thanks to
nonReentrant
. - The books (balances) are updated before the gold moves β CEI pattern at work.
βοΈ Before Mainnet Deployment
β
Run malicious reentrancy tests
β
Use audited OpenZeppelin dependencies
β
Add multisig for owner
functions
β
Verify on Etherscan or Polygonscan
β
Consider time delays for rescue functions
π Future Enhancements
- Support EIP-2612
permit()
deposits (gasless approvals) - Add timelocked withdrawals for long-term vaults
- Use Chainlink proof-of-reserve for transparency
- Create a dashboard UI for deposits and balances
π Summary
The FortKnoxVault demonstrates how a seemingly simple withdrawal function can open the door to reentrancy β and how to close it securely.
By applying the nonReentrant
modifier and Checks-Effects-Interactions, you can write secure, production-grade DeFi contracts that protect user funds and build trust.
Remember: in smart contracts, security is not optional β itβs the foundation of trust.
π§± Learning Outcome
After this project, youβll understand:
- How reentrancy works and how to stop it
- How to design safe withdrawal logic
- How to apply Solidityβs security best practices
- How to test for common vulnerabilities
π§© #30DaysOfSolidity Recap
Day 19: Signature-Based Authentication
Day 20: Secure Digital Vault (FortKnox)
Day 21: Create Your Own Digital Collectibles (NFTs) π
π Connect With Me
π¬ Dev.to: @sauravkumar8178
π¦ Twitter (X): @sauravk8178
πΌ LinkedIn: Saurav Kumar
Top comments (0)