π 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
βοΈ 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
πΎ 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);
}
}
π§ͺ 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();
}
}
Run your tests:
forge test -vv
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();
}
}
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
π 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)