DEV Community

Cover image for 🏦 Day 11 of #30DaysOfSolidity β€” Building a Secure Vault with Ownable + VaultMaster πŸ”
Saurav Kumar
Saurav Kumar

Posted on

🏦 Day 11 of #30DaysOfSolidity β€” Building a Secure Vault with Ownable + VaultMaster πŸ”

Hey everyone πŸ‘‹
Welcome to Day 11 of my #30DaysOfSolidity challenge!

Today, we’ll build a Secure Vault contract where only the owner (master key holder) can control the funds.
This project demonstrates Solidity inheritance, access control, and security best practices for managing ETH securely β€” just like real-world DeFi vaults.


🧩 What We'll Build

We'll split our logic into two contracts:

  1. Ownable β€” handles ownership and access control.
  2. VaultMaster β€” inherits Ownable and manages Ether deposits and withdrawals.

This modular design makes your code reusable, secure, and easy to maintain β€” following production-level smart contract architecture.


πŸ—‚οΈ Project Structure

Here’s how the folder structure looks:

day-11-vault/
β”‚
β”œβ”€β”€ contracts/
β”‚   β”œβ”€β”€ Ownable.sol
β”‚   └── VaultMaster.sol
β”‚
β”œβ”€β”€ scripts/
β”‚   └── deploy.js
β”‚
β”œβ”€β”€ test/
β”‚   └── vaultmaster.test.js
β”‚
β”œβ”€β”€ hardhat.config.js
└── package.json
Enter fullscreen mode Exit fullscreen mode

βš™οΈ Contract 1: Ownable.sol

The Ownable contract manages ownership and provides the onlyOwner modifier used for access control.

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

/// @title Ownable - Lightweight ownership control base contract
/// @notice Provides an `onlyOwner` modifier and ownership transfer functionality
contract Ownable {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor() {
        _owner = msg.sender;
        emit OwnershipTransferred(address(0), _owner);
    }

    function owner() public view returns (address) {
        return _owner;
    }

    modifier onlyOwner() {
        require(msg.sender == _owner, "Ownable: caller is not the owner");
        _;
    }

    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is zero address");
        emit OwnershipTransferred(_owner, newOwner);
        _owner = newOwner;
    }

    function renounceOwnership() public onlyOwner {
        emit OwnershipTransferred(_owner, address(0));
        _owner = address(0);
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ’° Contract 2: VaultMaster.sol

This contract acts as our digital safe. It allows anyone to deposit ETH, but only the owner can withdraw or transfer ownership.

It also includes:

  • Reentrancy protection
  • Event logging
  • Balance visibility
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "./Ownable.sol";

/// @title VaultMaster - Secure vault controlled by the contract owner
/// @notice Allows only the owner to withdraw or transfer ownership
contract VaultMaster is Ownable {
    uint256 private _status;
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    event Deposited(address indexed sender, uint256 amount);
    event Withdrawn(address indexed to, uint256 amount);

    constructor() {
        _status = _NOT_ENTERED;
    }

    modifier nonReentrant() {
        require(_status == _NOT_ENTERED, "VaultMaster: reentrant call");
        _status = _ENTERED;
        _;
        _status = _NOT_ENTERED;
    }

    receive() external payable {
        require(msg.value > 0, "VaultMaster: no ETH sent");
        emit Deposited(msg.sender, msg.value);
    }

    function deposit() external payable {
        require(msg.value > 0, "VaultMaster: no ETH sent");
        emit Deposited(msg.sender, msg.value);
    }

    function withdraw(uint256 amount, address payable to) external onlyOwner nonReentrant {
        require(to != address(0), "VaultMaster: zero address");
        require(address(this).balance >= amount, "VaultMaster: insufficient balance");

        (bool sent, ) = to.call{value: amount}("");
        require(sent, "VaultMaster: transfer failed");
        emit Withdrawn(to, amount);
    }

    function withdrawAll(address payable to) external onlyOwner nonReentrant {
        require(to != address(0), "VaultMaster: zero address");
        uint256 bal = address(this).balance;
        require(bal > 0, "VaultMaster: empty vault");

        (bool sent, ) = to.call{value: bal}("");
        require(sent, "VaultMaster: transfer failed");
        emit Withdrawn(to, bal);
    }

    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ Test Script β€” vaultmaster.test.js

Here’s a sample Hardhat test to verify deposits, withdrawals, and access control.

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

describe("VaultMaster", function () {
  let owner, alice, vault;

  beforeEach(async () => {
    [owner, alice] = await ethers.getSigners();
    const VaultMaster = await ethers.getContractFactory("VaultMaster");
    vault = await VaultMaster.deploy();
    await vault.deployed();
  });

  it("accepts deposits", async () => {
    await alice.sendTransaction({ to: vault.address, value: ethers.utils.parseEther("1") });
    const balance = await ethers.provider.getBalance(vault.address);
    expect(balance).to.equal(ethers.utils.parseEther("1"));
  });

  it("only owner can withdraw", async () => {
    await alice.sendTransaction({ to: vault.address, value: ethers.utils.parseEther("1") });
    await expect(vault.connect(alice).withdraw(ethers.utils.parseEther("1"), alice.address))
      .to.be.revertedWith("Ownable: caller is not the owner");
  });
});
Enter fullscreen mode Exit fullscreen mode

πŸš€ Deployment Script β€” deploy.js

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

async function main() {
  const VaultMaster = await ethers.getContractFactory("VaultMaster");
  const vault = await VaultMaster.deploy();
  await vault.deployed();

  console.log("VaultMaster deployed to:", vault.address);
}

main();
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ How It Works

  1. Deploy the VaultMaster contract β€” deployer becomes the owner automatically.
  2. Deposit ETH β€” anyone can send Ether to the contract.
  3. Withdraw ETH β€” only the owner can withdraw or transfer ownership.
  4. Reentrancy guard ensures no malicious multiple withdrawals.

πŸ” Security Highlights

  • onlyOwner modifier restricts sensitive functions.
  • nonReentrant modifier prevents reentrancy attacks.
  • Events make transactions auditable on-chain.
  • Ownership can be safely transferred or renounced.

🧭 Real-World Use Cases

  • DAO or company treasuries.
  • Multi-sig vaults with additional governance.
  • Secure smart contract fund storage.
  • DeFi liquidity safes or staking pools.

🧠 Future Enhancements

  • Add ERC20 token support.
  • Add multi-sig or time-lock functionality.
  • Integrate OpenZeppelin’s audited libraries for production.
  • Build a frontend dashboard for deposits and withdrawals.

🏁 Conclusion

In this project, we explored:

  • Solidity inheritance and access control
  • Writing modular and secure contracts
  • Building a Vault that follows production-grade patterns

A small yet powerful step toward writing secure Web3 smart contracts.


πŸ’¬ What do you think of today’s project?
Drop your thoughts and feedback in the comments πŸ‘‡

πŸ”— Read More β€” #30DaysOfSolidity Series by Saurav Kumar

Top comments (0)