Let's build a School Ticket System with the Diamond Pattern in Solidity 🚀

Hey everyone! 😊 Welcome back to my blog. Today, I’m walking you through how I implemented a School Ticket System using the Diamond Pattern in Solidity.

For anyone looking to understand modular smart contracts and Solidity best practices, the Diamond Pattern is an absolute game-changer. If you’ve ever wanted to build scalable, upgradeable, and efficient smart contracts, this pattern is the way to go.

I’ll walk you through the entire implementation of a School Ticket System which is a real-world example where we simulate a digital ticketing system (using tokens, of course 😉). And by the end of this article, you’ll have a gist of how ERC20 tokens and the Diamond Pattern can work together to create something cool.

A huge shout-out to my tutor OxPamPam and the Grazac Academy for guiding me through this assignment. Thanks to their help, I was able to take a deep dive into the Diamond Pattern and apply it practically. 🙌


What’s the Diamond Pattern?

Before we jump into the code, let’s first take a minute to talk about what the Diamond Pattern is and why it’s so useful in Solidity.

Imagine you're building a big, complicated smart contract. Over time, it can become harder to manage and update, especially if you want to add new features or change existing ones. The Diamond Pattern solves this problem by breaking the contract into smaller, manageable parts (called facets) while keeping the data in a central location. This allows us to:

  1. Modularize our contract—divide it into smaller pieces (facets) that handle specific tasks.

  2. Upgrade just the parts that need changing without messing with the entire contract.

  3. Optimize the contract for better scalability and easier maintenance.


Use Case: The School Ticket System 🎟️

Now let’s get practical! I decided to implement a School Ticket System, where we simulate digital tickets (ERC20 tokens). Students can buy, transfer, approve, and mint tickets—just like how event tickets work in real life.

For this, we used the Diamond Pattern to keep everything clean, modular, and upgradable. Here's how the system works:

  • Students can buy tickets (transfer tokens).

  • Students can transfer tickets to others.

  • Students can approve others to buy tickets on their behalf.

  • The school can mint new tickets (tokens) when needed (via the faucet function).


Let’s start coding

Let me walk you through the implementation. I’m going to break it down into smaller, digestible parts.

1. ERC20 Storage Facet (Where the magic happens 🔮)

First up is the storage where we keep all our data. In the ERC20Storage library, we define the basic details of our tickets (name, symbol, total supply, etc.) and implement the core functions for transferring tickets, approving ticket purchases, and more.

solidityCopy// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

library ERC20Storage {
    bytes32 constant ERC20_STORAGE_POSITION = keccak256("erc20.school.ticket.storage");

    struct Token {
        string name; // Name of the token (e.g., "School Event Ticket")
        string symbol; // Symbol for the token (e.g., "SET")
        uint8 decimals; // Decimals for the token (e.g., 18)
        uint256 totalSupply; // Total supply of tickets in circulation
        mapping(address => uint256) balances; // Mapping of student ticket balances
        mapping(address => mapping(address => uint256)) allowances; // Who can spend tickets on behalf of others
    }

    function getERC20Storage() internal pure returns (Token storage token) {
        bytes32 position = ERC20_STORAGE_POSITION;
        assembly {
            token.slot := position
        }
    }

    // Transfer function to send tickets (tokens) from one student to another
    function transfer(address recipient, uint256 amount) external returns (bool) {
        Token storage token = getERC20Storage();
        require(token.balances[msg.sender] >= amount, "Insufficient ticket balance");

        token.balances[msg.sender] -= amount; // Subtract tickets from the sender
        token.balances[recipient] += amount; // Add tickets to the recipient

        return true;
    }

    // Approve function to allow another student to spend tickets on your behalf
    function approve(address spender, uint256 amount) external returns (bool) {
        Token storage token = getERC20Storage();
        token.allowances[msg.sender][spender] = amount;
        return true;
    }

    // Check how many tickets a spender can spend on behalf of the owner
    function allowance(address owner, address spender) external view returns (uint256) {
        Token storage token = getERC20Storage();
        return token.allowances[owner][spender];
    }

    // Check the ticket balance of a student
    function balanceOf(address account) external view returns (uint256) {
        Token storage token = getERC20Storage();
        return token.balances[account];
    }

    // Function to mint new tickets (for example, giving out free tickets to students)
    function faucet(address to, uint256 amount) external {
        Token storage token = getERC20Storage();
        require(to != address(0), "Cannot mint tickets to zero address");

        token.balances[to] += amount; // Add minted tickets to the recipient's balance
        token.totalSupply += amount; // Increase the total supply of tickets
    }
}
  • ERC20Storage keeps the ticket data: the token’s name, symbol, balances, allowances, and total supply.

  • The key functions here are transfer, approve, and faucet—everything needed to handle the tickets.


2. School Ticket System Contract (The Interface for the System 🏫)

Next, we have the SchoolTicketSystem contract. This contract provides functions for interacting with the storage and managing the tickets (just like a real-world school ticket system).

solidityCopy// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "./ERC20Storage.sol"; // Import the storage library

contract SchoolTicketSystem {
    constructor(string memory name, string memory symbol, uint8 decimals) {
        ERC20Storage.Token storage token = ERC20Storage.getERC20Storage();
        token.name = name;
        token.symbol = symbol;
        token.decimals = decimals;
    }

    // Function to get the total supply of tickets in circulation
    function totalSupply() external view returns (uint256) {
        ERC20Storage.Token storage token = ERC20Storage.getERC20Storage();
        return token.totalSupply; // Return the total supply of tickets
    }

    // Function to get the ticket balance of a specific student
    function balanceOf(address student) external view returns (uint256) {
        return ERC20Storage.balanceOf(student); // Get the student's balance
    }

    // Function to allow a student to transfer tickets to another
    function transfer(address recipient, uint256 amount) external returns (bool) {
        return ERC20Storage.transfer(recipient, amount); // Transfer tickets
    }

    // Function to approve someone else to spend tickets on your behalf
    function approve(address spender, uint256 amount) external returns (bool) {
        return ERC20Storage.approve(spender, amount); // Approve spender
    }

    // Function to check how many tickets a spender is allowed to spend
    function allowance(address owner, address spender) external view returns (uint256) {
        return ERC20Storage.allowance(owner, spender); // Check allowance
    }

    // Function to mint new tickets to a student (like a faucet)
    function faucet(address to, uint256 amount) external {
        ERC20Storage.faucet(to, amount); // Mint new tickets to the recipient
    }
}
  • SchoolTicketSystem is the main contract where we interact with the functions like transfer, approve, and faucet.

  • The constructor sets the token’s name, symbol, and decimals.


Why the Diamond Pattern?

You might be wondering, “Why not just have all this in one contract?” The Diamond Pattern makes it easier to manage and upgrade the contract over time.

  • Modular: Each piece of functionality is separated into its own facet, making it easier to modify or upgrade.

  • Upgradable: If we want to add new features (like VIP tickets or ticket expiration dates), we can do that without rewriting the entire contract.

  • Efficient: The pattern keeps everything organized, so we don’t get lost in a single, massive contract.


Wrapping Up 🎉

This was a fun and educational assignment with Solidity and the Diamond Pattern. By breaking down the contract into modular facets and keeping the data in one central location, I was able to create a scalable, upgradable, and efficient school ticket system that mimics real-world scenarios.

I want to give a huge shoutout to my tutor, who guided me through the process. If you have any questions or thoughts, feel free to drop them in the comments below. 😊 I’d love to hear from you! (I’m pretty much still a beginner with solidity, but I’m looking forward to building cooler stuff)

Until next time, happy coding! 👩‍💻👨‍💻


16
Subscribe to my newsletter

Read articles from Oladetoun Gbemisola directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Oladetoun Gbemisola
Oladetoun Gbemisola

Frontend Developer from Nigeria