DEV Community

Cover image for Optimizing Solidity Smart Contracts & Hardhat Testing – MultiUserBank Edition
yo yo ben zhou
yo yo ben zhou

Posted on

Optimizing Solidity Smart Contracts & Hardhat Testing – MultiUserBank Edition

🚀 Introduction

For today, I transformed my smart contract from a single-user bank to a multi-user bank, optimized gas costs, secured ETH transfers, and gained a deep understanding of reentrancy protection.


Today's Highlights

Implemented user-specific balances with mapping(address => uint)

Optimized storage access to reduce SLOAD gas costs

Replaced .transfer() with .call{value: amount}("") for secure ETH transfers

Enhanced Hardhat test coverage with balance tracking

Deployed contracts dynamically with deploy.js

Understood and mitigated Reentrancy Attacks!


1️⃣ Upgrading to Multi-User Bank

🔴 Problem: Single-User Balance Tracking

Initially, my contract tracked one balance for the entire contract, meaning all users shared the same balance.

uint public balance; // Tracks one balance for all users
Enter fullscreen mode Exit fullscreen mode

🔴 Issue: All users shared a single balance, making it unrealistic for a real banking system.

💡 Solution: Use a mapping to Track Balances Per User

mapping(address => uint) balance;
Enter fullscreen mode Exit fullscreen mode

✅ Each user now has their own balance

✅ Deposits and withdrawals are specific to each address

Updated deposit function:

function deposit() public payable {
    require(msg.value > 0, "Deposit money must be higher than zero");
    balance[msg.sender] += msg.value;
    emit Deposit(msg.sender, msg.value, balance[msg.sender]);
}
Enter fullscreen mode Exit fullscreen mode

✅ Each user’s balance is updated independently!


2️⃣ Gas Optimization: Reduce Storage Reads (SLOAD)

Storage reads (SLOAD) in Solidity are expensive, so minimizing them reduces gas costs.

🔴 Original code with redundant SLOAD:

uint current_balance = balance[msg.sender];  
require(amount <= current_balance, "Insufficient balance");  
balance[msg.sender] = current_balance - amount;  
Enter fullscreen mode Exit fullscreen mode

🔴 Problem:** Reads storage twice (extra gas usage).

💡 Solution: Read from storage once, then modify in memory**

uint userBalance = balance[msg.sender];  
require(amount <= userBalance, "Insufficient balance");  
balance[msg.sender] = userBalance - amount;  
Enter fullscreen mode Exit fullscreen mode

✅ Storage access reduced from two to one (saving gas)!


3️⃣ Safer ETH Transfers: Using .call{value: amount}("") Instead of .transfer()

Initially, I used:

payable(msg.sender).transfer(amount);
Enter fullscreen mode Exit fullscreen mode

🔴 Problem:

  • .transfer() has a gas limit of 2300, which **may fail if the receiving address is a contract.
  • It does not return a success/failure flag, making debugging harder.

💡 Solution: Use .call{value: amount}("")

(bool sent, ) = payable(msg.sender).call{value: amount}("");
require(sent, "Transfer failed");
Enter fullscreen mode Exit fullscreen mode

✅ Handles ETH transfers more safely!
✅ Prevents unexpected failures due to gas limits!


4️⃣ Reentrancy Protection: Why This Version is Safe

I deepened my understanding of reentrancy vulnerabilities today!

🔴 What is a Reentrancy Attack?

A malicious contract can repeatedly call the withdrawal function before the balance updates, draining the contract’s funds, we may extend this topic in the future posts with more detailed examples.

🚨 Vulnerable Code Pattern:

(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Transfer failed");
balance[msg.sender] -= amount;
Enter fullscreen mode Exit fullscreen mode

🔴 Problem: The contract sends ETH before updating the balance, allowing reentrancy!

💡 Solution: Update the Balance Before Sending ETH

uint userBalance = balance[msg.sender];
require(amount <= userBalance, "Insufficient balance");

// ✅ Update balance FIRST before sending ETH
balance[msg.sender] = userBalance - amount;

(bool sent, ) = payable(msg.sender).call{value: amount}("");
require(sent, "Transfer failed");

emit Withdrawal(msg.sender, amount, balance[msg.sender]);
Enter fullscreen mode Exit fullscreen mode

✅ Reentrancy attack is impossible because the balance updates before ETH is sent!


5️⃣ Writing and Running Hardhat Tests

After improving the contract, I wrote comprehensive Hardhat tests.

Key Test Cases:

✅ Deposits update the correct user’s balance

✅ Withdrawals succeed and update the user’s balance correctly

✅ Withdrawals fail when balance is insufficient
✅ Gas fees are deducted correctly

Hardhat Test File (multiUserBank.test.js):

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("MultiUserBank", function () {
  let bank, owner, addr1, addr2;

  beforeEach(async function () {
    [owner, addr1, addr2] = await ethers.getSigners();
    const Bank = await ethers.getContractFactory("MultiUserBank");
    bank = await Bank.deploy();
    await bank.waitForDeployment();
  });

  it("should allow user-specific deposits", async function () {
    await bank.connect(addr1).deposit({ value: ethers.parseEther("1") });
    expect(await bank.getUserBankBalance(addr1.address)).to.equal(ethers.parseEther("1"));
  });

  it("should allow withdrawals and update balances correctly", async function () {
    await bank.connect(addr1).deposit({ value: ethers.parseEther("1") });

    const beforeBankBalance = await bank.getUserBankBalance(addr1.address);
    const beforeEthBalance = await ethers.provider.getBalance(addr1.address);

    const tx = await bank.connect(addr1).withdrawal(ethers.parseEther("0.5"));
    const receipt = await tx.wait();
    const gasUsed = BigInt(receipt.gasUsed) * BigInt(receipt.gasPrice);

    const afterBankBalance = await bank.getUserBankBalance(addr1.address);
    expect(afterBankBalance).to.equal(beforeBankBalance - ethers.parseEther("0.5"));

    const afterEthBalance = await ethers.provider.getBalance(addr1.address);
    expect(afterEthBalance).to.equal(beforeEthBalance + ethers.parseEther("0.5") - gasUsed);
  });

  it("should prevent overdrafts", async function () {
    await expect(bank.connect(addr1).withdrawal(ethers.parseEther("2"))).to.be.revertedWith(
      "Insufficient balance"
    );
  });
});
Enter fullscreen mode Exit fullscreen mode

✅ Thorough tests ensure contract safety!
✅ Validates ETH balance changes post-withdrawal!


📌 Final Thoughts:

Today’s Solidity deep dive helped me:

✅ Upgrade to a Multi-User Banking System using mapping
✅ Prevent Reentrancy Attacks by updating balances before sending ETH

✅ Reduce gas costs by optimizing storage access

✅ Deploy contracts dynamically instead of hardcoding deployments

Follow my Solidity progress on GitHub, Dev.to, and Medium! 🚀

🌍 Join Me on This Journey!
If you're also learning Solidity, Smart Contracts, or blockchain development, let's connect!
📌GitHub: https://github.com/benzdriver
📌LinkedIn: https://www.linkedin.com/in/ziyan-zhou/
💡 Let’s build the future of blockchain together! 🚀

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Top comments (1)

Collapse
 
rollingindo profile image
Zerod0wn Gaming

Your breakdown of Solidity optimizations is well-structured and covers critical security improvements, especially around gas efficiency and reentrancy protection.

For developers interested in taking security a step further, Oasis Network’s Sapphire provides the only confidential EVM in production. This allows smart contracts to process sensitive data without exposing it on-chain, enabling MEV-resistant DeFi, private transactions, and secure user authentication.

Privacy is often overlooked in Solidity development, but as Web3 adoption grows, ensuring confidential and secure computation will become essential. If you're working on dApps that require secure data handling, Sapphire’s confidential smart contracts could be a valuable addition.

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay