DEV Community

Cover image for 🏦 Day 14 of #30DaysOfSolidity β€” Building a Smart Bank with Modular Deposit Boxes (Interface + Foundry)
Saurav Kumar
Saurav Kumar

Posted on

🏦 Day 14 of #30DaysOfSolidity β€” Building a Smart Bank with Modular Deposit Boxes (Interface + Foundry)

🌟 Introduction

Welcome back to Day 14 of our #30DaysOfSolidity challenge!
Today, we’re going beyond simple tokens and vaults β€” we’re building a Smart Bank where users can store secrets, transfer ownership, and interact through a VaultManager.

Imagine a digital locker system on blockchain β€” where each locker (deposit box) can be:

  • Basic (free)
  • Premium (paid)
  • Time-Locked (retrievable only after a certain time)

This project will teach you interface design, contract modularity, and inter-contract communication β€” all essential for scalable dApps.


🧱 Concept Overview

We’ll build a modular banking system using Solidity where:

  • Each Deposit Box (Basic, Premium, TimeLocked) follows a common interface.
  • A VaultManager contract can deploy and interact with any box in a unified way.
  • Each box supports ownership transfer β€” just like handing over your locker key.

🧠 Key Concepts You’ll Learn

  • How to design and implement interfaces in Solidity.
  • How to build modular and scalable smart contracts.
  • Securely transfer ownership between users.
  • Use ETH payments, time locks, and contract composition.
  • Deploy and test contracts using Foundry.

🧩 Project Structure

day14-smart-bank/
β”‚
β”œβ”€β”€ src/
β”‚   └── DepositBoxes.sol
β”‚
β”œβ”€β”€ script/
β”‚   └── Deploy.s.sol
β”‚
β”œβ”€β”€ test/
β”‚   └── DepositBoxes.t.sol
β”‚
└── foundry.toml
Enter fullscreen mode Exit fullscreen mode

βš™οΈ Foundry Setup

Foundry is a blazing-fast Ethereum development toolkit built in Rust.
If you haven’t set it up yet:

# 1️⃣ Install Foundryup
curl -L https://foundry.paradigm.xyz | bash
foundryup

# 2️⃣ Create new project
forge init day14-smart-bank

# 3️⃣ Enter project directory
cd day14-smart-bank

# 4️⃣ Build contracts
forge build
Enter fullscreen mode Exit fullscreen mode

πŸ’Ύ Solidity Code β€” Deposit Boxes and Vault Manager

Below is the full code implementing three deposit boxes and a unified VaultManager.
Paste this into src/DepositBoxes.sol.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/// @title Smart Bank β€” Modular Deposit Boxes + VaultManager
/// @notice Build flexible, secure, and interface-driven storage boxes on-chain.
/// @dev For learning: never store plaintext secrets on-chain.

abstract contract Ownable {
    address private _owner;
    event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);
    constructor() { _transferOwnership(msg.sender); }
    modifier onlyOwner() { require(msg.sender == _owner, "Not owner"); _; }
    function owner() public view returns (address) { return _owner; }
    function transferOwnership(address newOwner) public onlyOwner {
        _transferOwnership(newOwner);
    }
    function _transferOwnership(address newOwner) internal {
        _owner = newOwner;
        emit OwnershipTransferred(msg.sender, newOwner);
    }
}

interface IDepositBox {
    function storeSecret(bytes calldata secret) external payable;
    function retrieveSecret() external view returns (bytes memory);
    function transferBoxOwnership(address newOwner) external;
    function owner() external view returns (address);
}

contract BasicBox is Ownable, IDepositBox {
    bytes private _secret;
    event SecretStored(address indexed by, uint256 timestamp);
    constructor(address _initOwner) { _transferOwnership(_initOwner); }
    function storeSecret(bytes calldata secret) external payable {
        _secret = secret; emit SecretStored(msg.sender, block.timestamp);
    }
    function retrieveSecret() external view onlyOwner returns (bytes memory) {
        return _secret;
    }
    function transferBoxOwnership(address newOwner) external onlyOwner {
        transferOwnership(newOwner);
    }
}

contract PremiumBox is Ownable, IDepositBox {
    bytes private _secret;
    uint256 public storeFee;
    uint256 public collectedFees;
    event SecretStored(address indexed by, uint256 fee);
    constructor(address _initOwner, uint256 _fee) {
        _transferOwnership(_initOwner);
        storeFee = _fee;
    }
    function storeSecret(bytes calldata secret) external payable {
        require(msg.value >= storeFee, "Fee too low");
        collectedFees += msg.value;
        _secret = secret;
        emit SecretStored(msg.sender, msg.value);
    }
    function retrieveSecret() external view onlyOwner returns (bytes memory) {
        return _secret;
    }
    function transferBoxOwnership(address newOwner) external onlyOwner {
        transferOwnership(newOwner);
    }
    function withdrawFees(address payable to) external onlyOwner {
        uint256 amt = collectedFees; collectedFees = 0;
        (bool ok,) = to.call{value: amt}("");
        require(ok, "Transfer failed");
    }
}

contract TimeLockedBox is Ownable, IDepositBox {
    bytes private _secret;
    uint256 public unlockTime;
    event SecretStored(address indexed by, uint256 unlockTime);
    constructor(address _initOwner, uint256 _unlockTime) {
        _transferOwnership(_initOwner);
        unlockTime = _unlockTime;
    }
    function storeSecret(bytes calldata secret) external payable {
        _secret = secret; emit SecretStored(msg.sender, unlockTime);
    }
    function retrieveSecret() external view onlyOwner returns (bytes memory) {
        require(block.timestamp >= unlockTime, "Locked");
        return _secret;
    }
    function transferBoxOwnership(address newOwner) external onlyOwner {
        transferOwnership(newOwner);
    }
}

contract VaultManager {
    struct Box { address box; string boxType; }
    Box[] public boxes;
    event BoxCreated(address box, string boxType);
    event SecretStored(address box, address by);

    function createBasicBox() external returns (address) {
        BasicBox box = new BasicBox(msg.sender);
        boxes.push(Box(address(box), "BasicBox"));
        emit BoxCreated(address(box), "BasicBox");
        return address(box);
    }

    function createPremiumBox(uint256 fee) external returns (address) {
        PremiumBox box = new PremiumBox(msg.sender, fee);
        boxes.push(Box(address(box), "PremiumBox"));
        emit BoxCreated(address(box), "PremiumBox");
        return address(box);
    }

    function createTimeLockedBox(uint256 unlockTime) external returns (address) {
        TimeLockedBox box = new TimeLockedBox(msg.sender, unlockTime);
        boxes.push(Box(address(box), "TimeLockedBox"));
        emit BoxCreated(address(box), "TimeLockedBox");
        return address(box);
    }

    function storeOnBox(address box, bytes calldata secret) external payable {
        IDepositBox(box).storeSecret{value: msg.value}(secret);
        emit SecretStored(box, msg.sender);
    }

    function retrieveFromBox(address box) external view returns (bytes memory) {
        return IDepositBox(box).retrieveSecret();
    }

    function transferBoxOwnership(address box, address newOwner) external {
        IDepositBox(box).transferBoxOwnership(newOwner);
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ Testing in Foundry

Create a test file at test/DepositBoxes.t.sol.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import "../src/DepositBoxes.sol";

contract DepositBoxesTest is Test {
    VaultManager manager;
    address user = address(0x1);

    function setUp() public {
        manager = new VaultManager();
        vm.deal(user, 10 ether);
    }

    function testBasicBoxFlow() public {
        vm.startPrank(user);
        address box = manager.createBasicBox();
        manager.storeOnBox(box, "hello");
        bytes memory stored = manager.retrieveFromBox(box);
        assertEq(keccak256(stored), keccak256("hello"));
        vm.stopPrank();
    }
}
Enter fullscreen mode Exit fullscreen mode

Run your tests:

forge test -vv
Enter fullscreen mode Exit fullscreen mode

You’ll see successful output validating storage and retrieval logic.


πŸš€ Deploying with Foundry Script

Create a script at script/Deploy.s.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "forge-std/Script.sol";
import "../src/DepositBoxes.sol";

contract DeployVaultManager is Script {
    function run() external {
        vm.startBroadcast();
        VaultManager manager = new VaultManager();
        console.log("VaultManager deployed at:", address(manager));
        vm.stopBroadcast();
    }
}
Enter fullscreen mode Exit fullscreen mode

Deploy it to a testnet (e.g., Sepolia):

forge script script/Deploy.s.sol:DeployVaultManager \
--rpc-url $SEPOLIA_RPC_URL \
--private-key $PRIVATE_KEY \
--broadcast
Enter fullscreen mode Exit fullscreen mode

πŸ” Security Notes

⚠️ Do not store real secrets on-chain β€” all blockchain data is public.

βœ… Store encrypted secrets or hashes.
βœ… Use Reentrancy Guards if you expand payment features.
βœ… Validate ownership on every transfer.
βœ… Add pause or upgradeability features for production readiness.


πŸ’‘ Real-World Use Cases

  • Decentralized locker systems for documents or metadata.
  • Escrow contracts that release information after a time.
  • Tiered storage services (free vs. premium).
  • Secure on-chain key handover systems.

🧠 Key Takeaways

  • Interfaces enable plug-and-play contract architectures.
  • Each contract can evolve independently under a common standard.
  • Using Foundry makes development and testing fast, modular, and efficient.

With this foundation, you’re one step closer to mastering Solidity modular design patterns and building real-world dApps.


πŸ‘¨β€πŸ’» Author

Saurav Kumar
Blockchain Developer | #30DaysOfSolidity | Smart Contract Engineer
Follow me on Dev.to for more Solidity tutorials.

Top comments (0)