Skip to content

leonleerl/bank

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Internal Transfer

Detail code in this branch: https://github.com/leonleerl/bank/tree/1-add-bank-internal-transfer

At present, the funds can only be in or out. You can implement a function that allows users to directly transfer their balance within the bank to another address without having to withdraw money first and then deposit it.

  • Challenge: Learn how to securely handle balance increases and decreases in two accounts within a contract.
// successful scenario
function test_InternalTransfer_Success() public {
    // user1 deposits 5 ether, transfers 2 ether to user2
}

// failed scenario
function test_InternalTransfer_InsufficientBalance() public {
    // insufficient funds
}

function test_InternalTransfer_ZeroAddress() public {
    // transfer to zero address
}

function test_InternalTransfer_SelfTransfer() public {
    // transfer to self
}

function test_InternalTransfer_ZeroAmount() public {
    // zero amount
}

// boundry scenario
function test_InternalTransfer_AllBalance() public {
    // transfer all
}

function test_InternalTransfer_BankBalanceUnchanged() public {
    // check whether the bank balance is unchanged
}

Time-locked Deposit

Detail code in this branch: https://github.com/leonleerl/bank/tree/3-time-locked-deposit

Introducing time-based logic. Users can deposit money and set a withdrawal timeframe (e.g., 7 days later).

  • Challenge: Learning to use block.timestamp and handling time-based require logic.

Overview

Implemented a time-locked deposit mechanism that allows users to lock their funds for a specified duration and withdraw them only after the lock period expires.

Features Implemented

  • Create Fixed Deposits: Users can create fixed-term deposits with customizable lock periods (minimum 2 days)
  • Time-based Withdrawal Control: Funds can only be withdrawn after the lock period expires using block.timestamp
  • Multiple Deposits Support: Each user can maintain multiple fixed deposits simultaneously
  • Withdrawal Protection: Prevents duplicate withdrawals and early withdrawals before maturity
  • Event Tracking: Comprehensive event logging for deposit creation and withdrawals

Key Technical Components

Data Structure

struct FixedDeposit {
    uint256 amount;        // Deposit amount
    uint256 depositTime;   // Deposit creation timestamp
    uint256 unlockTime;    // Unlock timestamp (depositTime + duration)
    bool isWithdrawn;      // Withdrawal status flag
}

Core Functions

  • createFixedDeposit(uint256 _amount, uint256 _duration) - Create a new time-locked deposit
  • withdrawFixedDeposit(uint256 _depositIndex) - Withdraw a matured deposit
  • getFixedDepositDetails(uint256 _depositIndex) - Query deposit information

Security Features

  • ✅ Reentrancy protection using OpenZeppelin's ReentrancyGuard
  • ✅ Custom errors for gas-efficient error handling
  • ✅ Comprehensive input validation
  • ✅ Duplicate withdrawal prevention
  • ✅ Time-based access control using block.timestamp

Test Coverage

Comprehensive test suite covering:

  • ✅ Successful deposit creation and withdrawal
  • ✅ Early withdrawal rejection (before maturity)
  • ✅ Duplicate withdrawal prevention
  • ✅ Invalid deposit index handling
  • ✅ Multiple deposits management
  • ✅ Edge cases and boundary conditions

ERC20 Token Support

Detail code in this branch: https://github.com/leonleerl/bank/tree/5-erc20-support

Overview

Extended the bank contract to support ERC20 tokens in addition to ETH, enabling users to deposit, withdraw, and transfer multiple types of tokens within the bank system.

Features Implemented

Multi-Token Balance Management

  • Dual Balance System: Separate tracking for ETH (ethBalances) and ERC20 tokens (tokenBalances)
  • Multi-Token Support: Users can hold balances in multiple different ERC20 tokens simultaneously
  • Unified Interface: Consistent function patterns for both ETH and token operations

Core ERC20 Functions

Token Deposits

  • depositToken(address _token, uint256 _amount) - Deposit ERC20 tokens into the bank
  • Utilises the Approve-TransferFrom pattern for secure token transfers
  • Validates token address and amount before processing

Token Withdrawals

  • withdrawToken(address _token, uint256 _amount) - Withdraw ERC20 tokens from the bank
  • Checks both user balance and contract's token holdings
  • Implements CEI (Checks-Effects-Interactions) pattern for security

Internal Token Transfers

  • internalTransferToken(address _token, address _to, uint256 _amount) - Transfer tokens between users within the bank
  • No actual token movement on blockchain (gas-efficient)
  • Only updates internal balance ledger

Query Functions

  • getBankTotalTokenBalance(address _token) - View total token balance held by the bank
  • getUserTokenBalance(address _user, address _token) - View user's wallet token balance
  • tokenBalances(address user, address token) - View user's token balance in the bank (public mapping)

Technical Implementation

Data Structures

// Nested mapping for multi-token balance tracking
mapping(address => mapping(address => uint256)) public tokenBalances;
// tokenBalances[user][tokenAddress] = balance

// Enhanced FixedDeposit structure with token support
struct FixedDeposit {
    uint256 amount;
    address token;      // Token address (address(0) for ETH)
    uint256 depositTime;
    uint256 unlockTime;
    bool isWithdrawn;
}

Events

event LogDepositToken(address indexed sender, address indexed token, uint256 amount);
event LogWithdrawToken(address indexed user, address indexed token, uint256 amount);
event LogInternalTransferToken(address indexed from, address indexed to, address indexed token, uint256 amount);

Security Features

  • ✅ Reentrancy protection on all token deposit/withdrawal functions
  • ✅ Comprehensive input validation (zero address, zero amount, self-transfer checks)
  • ✅ Balance verification before transfers
  • ✅ CEI pattern implementation for state changes
  • ✅ Custom errors for gas-efficient error handling

Approve-TransferFrom Pattern

Understanding the Two-Step Process:

  1. User Approval (executed on the token contract):
   // User approves the bank to spend their tokens
   token.approve(bankAddress, amount);
  1. Bank Transfer (executed on the bank contract):
   // Bank transfers tokens from user to itself
   IERC20(token).transferFrom(msg.sender, address(this), amount);

Why This Pattern?

  • Security: Users explicitly authorise how much the contract can spend
  • Flexibility: One approval can cover multiple deposits
  • Standard: Universal pattern across all ERC20 DeFi protocols

Key Differences:

Function Use Case Direction
transferFrom(from, to, amount) Deposit tokens User → Bank
transfer(to, amount) Withdraw tokens Bank → User

Test Coverage

Mock Token Setup:

// Created MockToken contract for testing
contract MockToken is ERC20 {
    constructor() ERC20("MockToken", "MTK") {
        _mint(msg.sender, 1000000 * 10**18);
    }
    
    function mint(address _to, uint256 _amount) external {
        _mint(_to, _amount);
    }
}

Test Scenarios:

  • ✅ Successful token deposits with proper approval
  • ✅ Successful token withdrawals
  • ✅ Internal token transfers between users
  • ✅ Insufficient balance errors
  • ✅ Zero amount validation
  • ✅ Zero address validation
  • ✅ Self-transfer prevention
  • ✅ Bank balance verification after operations

Usage Example

// 1. User approves bank to spend tokens (on token contract)
USDT.approve(bankAddress, 1000 * 10**6);

// 2. User deposits tokens (on bank contract)
bank.depositToken(USDTAddress, 500 * 10**6);

// 3. User transfers tokens internally (on bank contract)
bank.internalTransferToken(USDTAddress, recipientAddress, 200 * 10**6);

// 4. User withdraws tokens (on bank contract)
bank.withdrawToken(USDTAddress, 100 * 10**6);

Future Enhancements

  • ERC20 support for fixed deposits (time-locked token deposits)
  • Token whitelist mechanism for supported assets
  • Batch operations for multiple tokens
  • SafeERC20 wrapper for additional safety with non-standard tokens
  • Interest-bearing token deposits

Interest Distribution System

Detail code in this branch: https://github.com/leonleerl/bank/tree/7-interest-distribution

Overview

Implemented a pull-based interest distribution mechanism for both ETH and ERC20 tokens. Users earn interest on their deposits based on an annual interest rate set by the contract owner. The system uses simple interest (not compound interest), where earned interest is accumulated separately and does not automatically reinvest into the principal.

Key Features

  • Pull-based Distribution: Users actively claim their interest rather than automatic distribution (gas-efficient and scalable)
  • Simple Interest Model: Interest = Principal × Rate × Time
  • Multi-Asset Support: Separate interest tracking for ETH and each ERC20 token
  • Dynamic Interest Calculation: Interest accumulates per second based on current balance and time elapsed

Architecture Choice: Pull vs Push

Push Model (Not Implemented)

// Iterates through all users - NOT SCALABLE
function distributeInterest() external onlyOwner {
    for (uint i = 0; i < allUsers.length; i++) {
        balances[allUsers[i]] += calculateInterest(allUsers[i]);
    }
}

Problems:

  • Gas cost grows linearly with user count
  • May exceed block gas limit with 1000+ users
  • Not viable in production DeFi

Pull Model (Implemented)

// User claims their own interest - SCALABLE
function claimInterest() external {
    uint256 interest = calculateAccruedInterest(msg.sender);
    // Transfer interest to user
}

Advantages:

  • Constant gas cost regardless of user count
  • Industry standard (used by Compound, Aave, Uniswap)
  • Infinitely scalable

Data Structures

ETH Interest Tracking

uint256 public annualInterestRate;  // Global interest rate (e.g., 5 = 5%)

// Last time interest was calculated for each user
mapping(address => uint256) public lastInterestUpdate;

// Accumulated but unclaimed interest for each user
mapping(address => uint256) public pendingInterest;

Token Interest Tracking

// Last update time: user → token → timestamp
mapping(address => mapping(address => uint256)) public tokenLastInterestUpdate;

// Pending interest: user → token → amount
mapping(address => mapping(address => uint256)) public tokenPendingInterest;

Core Mechanism: _updateInterest Implementation

The _updateInterest function is the heart of the interest system. It's called before every balance change to ensure accurate interest calculation.

Why Do We Need This?

Problem: Interest depends on three variables:

  1. Principal (changes with deposits/withdrawals)
  2. Interest Rate (fixed unless owner updates)
  3. Time (continuously increasing)

If we don't settle interest before balance changes, calculations become incorrect.

Example Scenario

Day 0: User deposits 100 ETH
Day 182.5: User deposits another 100 ETH
Day 365: How much interest should they have?

❌ WRONG (without _updateInterest):
Interest = 200 ETH × 10% × 365 days = 20 ETH

✅ CORRECT (with _updateInterest):
First half: 100 ETH × 10% × 182.5 days = 5 ETH
Second half: 200 ETH × 10% × 182.5 days = 10 ETH
Total: 15 ETH

Implementation Flow

function _updateInterest(address _user) internal {
    // Step 1: First-time initialization
    if (lastInterestUpdate[_user] == 0) {
        lastInterestUpdate[_user] = block.timestamp;
        return;  // No interest to calculate yet
    }

    // Step 2: Calculate time elapsed since last update
    uint256 timePassed = block.timestamp - lastInterestUpdate[_user];
    
    // Step 3: Get CURRENT principal (before it changes)
    uint256 principal = ethBalances[_user];
    
    // Step 4: Calculate NEW interest for this period
    // Formula: Interest = Principal × Rate × Time / Year
    uint256 newInterest = (principal * annualInterestRate * timePassed) / (365 days * 100);
    
    // Step 5: Add to pending interest "savings jar"
    pendingInterest[_user] += newInterest;
    
    // Step 6: Reset the clock
    lastInterestUpdate[_user] = block.timestamp;
    
    // Step 7: Emit event
    emit LogUpdateInterest(_user, newInterest);
}

Detailed Walkthrough

Timeline Example:

═══════════════════════════════════════════════════════════════
TIME:       Day 0          Day 182.5         Day 365
ACTION:     Deposit 100    Deposit 100       Query Interest
═══════════════════════════════════════════════════════════════

DAY 0 - First Deposit (100 ETH)
───────────────────────────────────────────────────────────────
deposit(100 ETH) called
  ↓
  _updateInterest(user)
    ├─ lastInterestUpdate[user] == 0  ✓ (first time)
    ├─ lastInterestUpdate[user] = block.timestamp (Day 0)
    └─ return (no interest yet)
  ↓
  ethBalances[user] += 100 ETH

STATE AFTER:
  ethBalances[user] = 100 ETH
  lastInterestUpdate[user] = Day 0
  pendingInterest[user] = 0


DAY 182.5 - Second Deposit (100 ETH)
───────────────────────────────────────────────────────────────
deposit(100 ETH) called
  ↓
  _updateInterest(user)  👈 CRITICAL: Called BEFORE balance changes
    ├─ timePassed = Day 182.5 - Day 0 = 182.5 days
    ├─ principal = 100 ETH  👈 Uses OLD balance
    ├─ newInterest = 100 × 10% × 182.5/365 = 5 ETH
    ├─ pendingInterest[user] += 5 ETH  👈 Save to "jar"
    └─ lastInterestUpdate[user] = Day 182.5  👈 Reset clock
  ↓
  ethBalances[user] += 100 ETH = 200 ETH  👈 NOW balance changes

STATE AFTER:
  ethBalances[user] = 200 ETH     (balance changed)
  lastInterestUpdate[user] = Day 182.5  (clock reset)
  pendingInterest[user] = 5 ETH   (saved first period interest)


DAY 365 - Query Interest
───────────────────────────────────────────────────────────────
getAccruedInterest(user) called
  ↓
  calculateAccruedInterest(user)
    ├─ timePassed = Day 365 - Day 182.5 = 182.5 days
    ├─ principal = 200 ETH  👈 Uses NEW balance
    ├─ newInterest = 200 × 10% × 182.5/365 = 10 ETH
    └─ return pendingInterest + newInterest
             = 5 ETH + 10 ETH = 15 ETH ✓

TOTAL INTEREST: 15 ETH
  = First period (100 ETH for 182.5 days) = 5 ETH
  + Second period (200 ETH for 182.5 days) = 10 ETH

The "Checkpoint Pattern"

This implementation follows the Checkpoint Pattern used in major DeFi protocols:

User Account Structure:
┌──────────────────────────┐
│ ethBalances             │ → Current principal (for calculating NEW interest)
├──────────────────────────┤
│ pendingInterest         │ → Interest "savings jar" (already earned)
├──────────────────────────┤
│ lastInterestUpdate      │ → Clock (when was interest last settled)
└──────────────────────────┘

Every time balance changes:

  1. Calculate interest with OLD principal
  2. Put interest in the "savings jar" (pendingInterest)
  3. Reset the clock (lastInterestUpdate)
  4. Then change the balance

When querying total interest:

Total Interest = Savings Jar + Recently Earned
               = pendingInterest + (currentBalance × rate × timeSinceLastUpdate)

Integration Points

ETH Operations:

function deposit() external payable {
    _updateInterest(msg.sender);  // Settle interest BEFORE deposit
    ethBalances[msg.sender] += msg.value;
}

function withdraw(uint256 _amount) external {
    _updateInterest(msg.sender);  // Settle interest BEFORE withdrawal
    ethBalances[msg.sender] -= _amount;
    // Transfer ETH
}

function internalTransfer(address _to, uint256 _amount) external {
    _updateInterest(msg.sender);  // Settle sender's interest
    _updateInterest(_to);          // Settle receiver's interest
    // Transfer balances
}

Token Operations:

function depositToken(address _token, uint256 _amount) external {
    _updateTokenInterest(msg.sender, _token);  // Settle before deposit
    // Transfer and update balance
}

function withdrawToken(address _token, uint256 _amount) external {
    _updateTokenInterest(msg.sender, _token);  // Settle before withdrawal
    // Transfer and update balance
}

function internalTransferToken(address _token, address _to, uint256 _amount) external {
    _updateTokenInterest(msg.sender, _token);  // Settle sender
    _updateTokenInterest(_to, _token);         // Settle receiver
    // Transfer balances
}

Core Functions

Interest Calculation (View Functions)

// Calculate total accrued interest for ETH
function calculateAccruedInterest(address _user) public view returns (uint256) {
    if (lastInterestUpdate[_user] == 0) return 0;
    
    uint256 timePassed = block.timestamp - lastInterestUpdate[_user];
    uint256 principal = ethBalances[_user];
    uint256 newInterest = (principal * annualInterestRate * timePassed) / (365 days * 100);
    
    return pendingInterest[_user] + newInterest;
}

// Calculate total accrued interest for a specific token
function calculateTokenAccruedInterest(address _user, address _token) 
    public view returns (uint256) 
{
    if (tokenLastInterestUpdate[_user][_token] == 0) return 0;
    
    uint256 timePassed = block.timestamp - tokenLastInterestUpdate[_user][_token];
    uint256 principal = tokenBalances[_user][_token];
    uint256 newInterest = (principal * annualInterestRate * timePassed) / (365 days * 100);
    
    return tokenPendingInterest[_user][_token] + newInterest;
}

Claiming Interest

// Claim ETH interest
function claimInterest() external nonReentrant {
    uint256 interest = calculateAccruedInterest(msg.sender);
    if (interest == 0) revert NoInterestToClaim();
    
    // Verify bank has sufficient balance
    if (address(this).balance < interest) {
        revert InsufficientFunds(address(this).balance, interest);
    }
    
    // Transfer interest to user
    (bool success,) = payable(msg.sender).call{value: interest}("");
    require(success, "Transfer failed");
    
    // Reset state
    lastInterestUpdate[msg.sender] = block.timestamp;
    pendingInterest[msg.sender] = 0;
    
    emit LogClaimInterest(msg.sender, interest);
}

// Claim token interest
function claimTokenInterest(address _token) external nonReentrant {
    uint256 interest = calculateTokenAccruedInterest(msg.sender, _token);
    if (interest == 0) revert NoInterestToClaim();
    
    // Verify bank has sufficient token balance
    uint256 bankBalance = IERC20(_token).balanceOf(address(this));
    if (bankBalance < interest) {
        revert InsufficientFunds(bankBalance, interest);
    }
    
    // Transfer interest to user
    bool success = IERC20(_token).transfer(msg.sender, interest);
    require(success, "Transfer failed");
    
    // Reset state
    tokenLastInterestUpdate[msg.sender][_token] = block.timestamp;
    tokenPendingInterest[msg.sender][_token] = 0;
    
    emit LogClaimTokenInterest(msg.sender, _token, interest);
}

Owner Functions

// Set annual interest rate (e.g., 5 = 5%)
function setInterestRate(uint256 _rate) external onlyOwner {
    annualInterestRate = _rate;
    emit LogSetInterestRate(msg.sender, _rate);
}

Events

event LogClaimInterest(address indexed user, uint256 amount);
event LogClaimTokenInterest(address indexed user, address indexed token, uint256 amount);
event LogSetInterestRate(address indexed owner, uint256 rate);
event LogUpdateInterest(address indexed user, uint256 interest);
event LogUpdateTokenInterest(address indexed user, address indexed token, uint256 interest);

Simple vs Compound Interest

Current Implementation: Simple Interest

Year 1: Principal 100 ETH → Interest 10 ETH (goes to pendingInterest)
Year 2: Principal 100 ETH → Interest 10 ETH (goes to pendingInterest)
Year 3: Principal 100 ETH → Interest 10 ETH (goes to pendingInterest)

Total Interest = 30 ETH
Total Assets = 100 ETH (principal) + 30 ETH (interest) = 130 ETH

Key Points:

  • Interest is stored in pendingInterest
  • Interest does NOT automatically add to principal
  • Next period calculates based on original principal
  • User must manually claim interest (pulled to wallet)

Compound Interest (Not Implemented):

Year 1: Principal 100 ETH → Interest 10 ETH → Principal becomes 110 ETH
Year 2: Principal 110 ETH → Interest 11 ETH → Principal becomes 121 ETH
Year 3: Principal 121 ETH → Interest 12.1 ETH → Principal becomes 133.1 ETH

Total Assets = 133.1 ETH (all in principal)

Interest Funding

Interest is paid from the bank's surplus (excess balance):

Bank Surplus = Total Bank Balance - Total User Deposits

Example:
- Total ETH in contract: 1000 ETH
- Total user deposits: 900 ETH
- Surplus available for interest: 100 ETH

Important: The contract verifies sufficient balance before paying interest to prevent insolvency.

Usage Example

// Owner sets 5% annual interest rate
bank.setInterestRate(5);

// User deposits 100 ETH
bank.deposit{value: 100 ether}();

// Wait 1 year (simulated)
vm.warp(block.timestamp + 365 days);

// Check accrued interest
uint256 interest = bank.getAccruedInterest(user);  // Returns 5 ETH

// Claim interest
bank.claimInterest();  // User receives 5 ETH to their wallet

// For tokens
token.approve(address(bank), 100 ether);
bank.depositToken(address(token), 100 ether);

vm.warp(block.timestamp + 365 days);

uint256 tokenInterest = bank.getTokenAccruedInterest(user, address(token));
bank.claimTokenInterest(address(token));

Security Considerations

  1. Reentrancy Protection: All claim functions use nonReentrant modifier
  2. Balance Verification: Checks bank has sufficient funds before paying interest
  3. CEI Pattern: Updates state before external calls
  4. Overflow Protection: Solidity 0.8+ has built-in overflow checks
  5. Access Control: Only owner can set interest rate

Comparison with Major DeFi Protocols

Compound Protocol:

  • Uses a global exchange rate that increases over time
  • Automatically compounds interest
  • More gas-efficient for high-frequency operations

Aave Protocol:

  • Uses a similar checkpoint pattern
  • Supports variable interest rates
  • More complex with liquidity provider mechanics

This Implementation:

  • Educational focus on core concepts
  • Simple interest for clarity
  • Direct mapping to traditional banking

Pausable & Blacklist Mechanism

Detail code see this branch: https://github.com/leonleerl/bank/tree/9-pausable-and-blacklist

Overview

Implemented comprehensive security controls for emergency response and compliance. The contract owner can pause all operations globally or blacklist specific addresses to prevent malicious activity, respond to security incidents, or meet regulatory requirements.

Architecture: Dual-Layer Protection

Security Layers:
┌─────────────────────────────────────┐
│  Layer 1: Global Pause (Emergency)  │ → All users affected
├─────────────────────────────────────┤
│  Layer 2: Blacklist (Targeted)      │ → Specific addresses
└─────────────────────────────────────┘

Design Philosophy:

  • Defense in Depth: Multiple security layers for comprehensive protection
  • Granular Control: Choose between global shutdown vs targeted blocking
  • Reversible Actions: Both mechanisms can be undone when issues are resolved
  • Minimal Impact: View functions remain accessible during security events

Features

1. Pausable Mechanism

Global emergency brake for the entire contract using OpenZeppelin's Pausable library.

Functionality:

  • Pause: Immediately freeze all state-changing operations
  • Unpause: Resume normal operations after issue resolution
  • Scope: Affects all users equally

Protected Operations:

  • ✅ Deposits (ETH & tokens)
  • ✅ Withdrawals (ETH & tokens)
  • ✅ Internal transfers
  • ✅ Fixed deposit creation/withdrawal
  • ✅ Interest claiming
  • ❌ View functions (remain accessible)

Access Control:

  • Only contract owner can pause/unpause
  • Implemented using OpenZeppelin's Ownable

2. Blacklist Mechanism

Targeted blocking of specific addresses for compliance or security.

Functionality:

  • Add to Blacklist: Block specific address from all operations
  • Remove from Blacklist: Restore address access
  • Query: Check if address is blacklisted

Protected Operations: Same as pausable, plus:

  • Transfer recipients are also checked (prevents sending to blacklisted addresses)

Access Control:

  • Only contract owner can manage blacklist
  • Zero address cannot be blacklisted (validation check)

Implementation Details

Data Structures

// Blacklist storage
mapping(address => bool) public blacklist;

Modifier Stack

function criticalOperation() 
    external 
    payable              // 1. Payment capability
    nonReentrant         // 2. Reentrancy protection
    whenNotPaused        // 3. Global pause check (higher priority)
    notBlacklisted       // 4. User-level check (lower priority)
{
    // Function logic
}

Modifier Order Rationale:

  1. payable - Language feature, must be first
  2. nonReentrant - Fundamental security, prevents reentrancy attacks
  3. whenNotPaused - Global switch, affects all users (check first for efficiency)
  4. notBlacklisted - User-specific, only relevant if contract is active

Custom Modifier

modifier notBlacklisted() {
    if (blacklist[msg.sender]) revert Blacklisted();
    _;
}

Bidirectional Transfer Protection

function internalTransfer(address _to, uint256 _amount) 
    external 
    whenNotPaused 
    notBlacklisted  // Checks sender
{
    // Explicit check for recipient
    if (blacklist[_to]) revert Blacklisted();
    
    // Transfer logic
}

Why check both sender and recipient?

  • Prevents blacklisted users from receiving funds
  • Prevents users from transferring to blacklisted addresses
  • Comprehensive compliance enforcement

Core Functions

Owner Functions:

// Pause contract (emergency)
function pause() external onlyOwner {
    _pause();
}

// Resume contract operations
function unpause() external onlyOwner {
    _unpause();
}

// Add address to blacklist
function addToBlacklist(address _user) external onlyOwner {
    if (_user == address(0)) revert ZeroAddress();
    blacklist[_user] = true;
    emit LogAddToBlacklist(_user);
}

// Remove address from blacklist
function removeFromBlacklist(address _user) external onlyOwner {
    if (_user == address(0)) revert ZeroAddress();
    blacklist[_user] = false;
    emit LogRemoveFromBlacklist(_user);
}

// Check blacklist status
function isBlacklisted(address _user) external view returns (bool) {
    return blacklist[_user];
}

Events

event LogAddToBlacklist(address indexed user);
event LogRemoveFromBlacklist(address indexed user);

Errors

error Blacklisted();

Real-World Use Cases

Scenario 1: Smart Contract Vulnerability

Timeline:
─────────────────────────────────────────────────────────
09:00  Security researcher reports critical reentrancy bug
09:05  Owner calls pause()
       → All deposits/withdrawals frozen
       → Attack vector neutralised
09:30  Development team analyses vulnerability
12:00  Fix deployed to new contract version
12:30  Migration plan announced to users
14:00  Owner calls unpause() on old contract for withdrawals only
16:00  All users migrated to new secure contract

Why Pause?

  • Immediate protection for all users
  • Prevents exploitation while fix is developed
  • Allows controlled migration process

Scenario 2: Suspicious Account Activity

Timeline:
─────────────────────────────────────────────────────────
10:00  Alert: Address 0x123... withdrawing unusual amounts
10:05  Owner calls addToBlacklist(0x123...)
       → Specific address blocked
       → Other users unaffected
10:30  Fraud investigation begins
14:00  Investigation reveals legitimate large withdrawal
14:15  Owner calls removeFromBlacklist(0x123...)
       → User access restored
       → No harm, no foul

Why Blacklist?

  • Surgical precision (only affects suspected address)
  • Minimal disruption to legitimate users
  • Reversible if investigation clears user

Scenario 3: Regulatory Compliance

Timeline:
─────────────────────────────────────────────────────────
11:00  Government order: Freeze assets of address 0xABC...
11:10  Owner calls addToBlacklist(0xABC...)
       → Compliance with legal requirement
       → Audit trail via blockchain events
       → Funds remain in contract (not confiscated)
30d    Legal case resolved
       Owner calls removeFromBlacklist(0xABC...)
       → User regains access to funds

Why This Approach?

  • Meets regulatory obligations
  • Transparent and auditable
  • Non-custodial (funds stay in contract)
  • Reversible when legally permitted

Scenario 4: Coordinated Attack

Timeline:
─────────────────────────────────────────────────────────
15:00  Multiple addresses exploit price oracle manipulation
15:03  Owner calls pause()
       → Global freeze
15:05  Owner identifies attacker addresses
15:10  Owner calls addToBlacklist() for each attacker
15:30  Oracle issue fixed
15:45  Owner calls unpause()
       → Legitimate users resume operations
       → Attackers remain permanently blocked

Why Both Mechanisms?

  • Pause: Immediate blanket protection
  • Blacklist: Targeted long-term blocking
  • Layered defense strategy

Security Considerations

1. Centralization Risk

  • Owner has significant power (pause/blacklist)
  • Mitigation: Implement multi-sig ownership
  • Best Practice: Establish clear governance procedures

2. Emergency Response Time

  • Critical: Owner must respond quickly to threats
  • Mitigation: 24/7 monitoring systems
  • Best Practice: Automated alerts for suspicious activity

3. False Positives

  • Risk: Legitimate users wrongly blacklisted
  • Mitigation: Clear appeal process
  • Best Practice: Investigate before blacklisting

4. View Function Accessibility

// ✅ CORRECT: View functions always accessible
function getUserBalance(address _user) external view returns (uint256) {
    return ethBalances[_user];
}

// ❌ WRONG: Don't restrict view functions
function getUserBalance(address _user) 
    external 
    view 
    whenNotPaused  // Don't do this!
    returns (uint256) 
{
    return ethBalances[_user];
}

Rationale:

  • Users should always see their balances
  • Transparency during security events
  • No security risk (read-only)
  • Maintains trust

Testing Coverage

Pause Mechanism Tests:

  • ✅ Prevents deposits when paused
  • ✅ Prevents withdrawals when paused
  • ✅ Allows operations after unpause
  • ✅ Only owner can pause/unpause
  • ✅ View functions work during pause

Blacklist Mechanism Tests:

  • ✅ Prevents blacklisted deposits
  • ✅ Prevents blacklisted withdrawals
  • ✅ Prevents transfers to blacklisted addresses
  • ✅ Allows operations after removal from blacklist
  • ✅ Only owner can manage blacklist
  • ✅ View functions work for blacklisted users

Combined Tests:

  • ✅ Pause takes precedence over blacklist
  • ✅ Blacklist remains active after unpause
  • ✅ Independent operation of both mechanisms

Comparison with Industry Standards

OpenZeppelin Pausable:

  • Industry-standard implementation
  • Battle-tested security
  • Gas-efficient
  • Widely audited

Similar Implementations:

  • Tether (USDT): Blacklist for regulatory compliance
  • Circle (USDC): Combined pause and blacklist
  • MakerDAO: Emergency shutdown mechanism

Our Implementation:

  • Follows best practices
  • Comprehensive protection
  • Educational clarity
  • Production-ready patterns

Usage Example

// Emergency: Vulnerability discovered
bank.pause();

// Specific threat: Malicious address identified
bank.addToBlacklist(0x1234...);

// Investigation: Address cleared
bank.removeFromBlacklist(0x1234...);

// Recovery: Issue resolved
bank.unpause();

// Compliance: Check status
bool isPaused = bank.paused();  // OpenZeppelin function
bool isBlocked = bank.isBlacklisted(userAddress);

Gas Optimization Notes

Modifier Order Impact:

// Efficient: Checks in order of likelihood to fail
whenNotPaused      // Global state, cheap to check
notBlacklisted     // Mapping lookup, also cheap

// Both are O(1) operations
// Order matters for failed transactions (saves gas on revert)

Learning Outcomes

  • Security Patterns: Multi-layered defense strategies
  • Access Control: Owner privileges and responsibility
  • OpenZeppelin Integration: Using audited libraries
  • Emergency Response: Incident handling procedures
  • Compliance: Meeting regulatory requirements
  • UX Considerations: Balancing security with accessibility
  • Testing Strategies: Comprehensive security test coverage

Critical Reminders

  1. Owner Security: Private key management is critical
  2. Response Time: Minutes matter in security incidents
  3. Communication: Inform users about security events
  4. Documentation: Maintain incident logs
  5. Testing: Regularly test pause/unpause procedures
  6. Transparency: Public communication about blacklist reasons
  7. Reversibility: Always plan for issue resolution
  8. Governance: Establish clear decision-making processes

About

A Solidity bank smart contract with ETH/ERC20 deposits, in-contract transfers, time-locked fixed deposits, and interest accrual. Built with Foundry and OpenZeppelin (Ownable, ReentrancyGuard, Pausable).

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors