Clock in Cairo: Complete Solution & Deep Dive Guide

Pyramids visible over buildings and street traffic

The Ultimate Guide to Time Manipulation in Cairo: Building a Clock from Scratch

Implementing a clock in Cairo involves creating a custom struct to represent time and leveraging traits for arithmetic operations like addition and subtraction. This requires careful management of time rollovers using modular arithmetic to ensure that adding minutes to 23:59 correctly results in 00:00, creating a robust and reusable time-handling component.

Ever found yourself wrestling with time-based logic in a smart contract? You might be building a decentralized game where actions are time-gated, a DeFi protocol with a vesting schedule, or any on-chain application that needs to answer "what time is it?" without caring about the date. The moment you need to add 15 minutes to 23:50, you realize it's not as simple as basic addition. The clock needs to "roll over," and handling these edge cases manually is a recipe for bugs.

This is a common hurdle for developers diving into Starknet development. Time is a fundamental concept, but representing it correctly and safely on-chain requires a thoughtful approach. A single miscalculation in a time-sensitive contract could have significant consequences. This guide cuts through that complexity. We will build a powerful, reusable, and gas-efficient Clock component from the ground up, teaching you not just the solution, but the core Cairo principles of structs, traits, and modular arithmetic that make it possible.


What is a Time-Handling Component in Cairo?

At its core, a time-handling component, like the Clock we will build, is a custom data structure designed to encapsulate the logic of time. Instead of scattering raw numbers for hours and minutes across your codebase, you create a single, authoritative source of truth. In Cairo, this is achieved by defining a struct.

A struct is a composite data type that groups together related variables under one name. For our Clock, this means bundling hours and minutes into a single, cohesive unit. This approach offers immense benefits:

  • Type Safety: You can pass a Clock object around your program, and the compiler ensures it's always a valid representation of time, not just some arbitrary numbers.
  • Encapsulation: All the logic for creating, manipulating, and displaying the time is contained within the Clock's implementation, making your code cleaner and easier to maintain.
  • Reusability: Once built, this Clock component can be imported and used in any project, saving you from reinventing the wheel.

But a data structure alone is static. To make it useful, we need to define its behavior. This is where Cairo's traits come in. Traits are like interfaces or contracts that define a set of methods a type must implement. For our Clock, we'll implement several key traits:

  • Add<felt252>: To define what happens when you add minutes (a felt252) to a Clock.
  • Sub<felt252>: To define how to subtract minutes from a Clock.
  • PartialEq: To check if two Clock instances represent the exact same time.
  • Display: To create a standardized, human-readable string representation, like "08:30".

By combining a struct with trait implementations, we elevate simple data into a smart, capable component that understands the rules of time.


Why Correct Time Arithmetic is Critical on Starknet

On a decentralized network like Starknet, precision and predictability are paramount. Smart contracts govern assets and execute irreversible transactions, so "close enough" is never good enough. Incorrect time arithmetic can introduce subtle but critical vulnerabilities.

Consider these scenarios:

  • DeFi Vesting Schedules: A contract releases tokens to investors over time. If the logic for calculating release dates is flawed due to improper time rollovers, tokens could be released too early or too late, breaking the terms of the agreement.
  • On-Chain Gaming: A game might have a "daily reward" system that resets at midnight (00:00). If adding 1 minute to 23:59 results in 24:00 instead of 00:00, the daily reset logic might never trigger, preventing players from claiming their rewards.
  • Time-Locked Vaults: A user locks funds that can only be withdrawn after a certain duration. An error in subtracting time could allow for premature withdrawals, defeating the purpose of the lock.

The key challenge is handling the cyclical nature of time. A day has 1440 minutes (24 hours * 60 minutes). Any calculation that exceeds this limit must "wrap around." This is a perfect use case for modular arithmetic, which is fundamental to cryptography and, by extension, to blockchain development. Mastering it for a simple component like a clock builds the foundational skills needed for more complex on-chain logic.


How to Build a Robust Clock in Cairo: A Step-by-Step Implementation

Let's dive into the code. We'll build our Clock component methodically, starting with the data structure and then layering on the behavior with traits. This entire implementation is part of the exclusive Kodikra Cairo Learning Path, designed to build practical skills.

Step 1: Defining the `Clock` Struct

First, we define the structure that will hold our data. It's simple: one field for hours and one for minutes. We'll use u8 for both, as hours only go from 0-23 and minutes from 0-59, making this type a perfect and gas-efficient fit.


use core::ops::{Add, Sub};

#[derive(Copy, Drop, PartialEq)]
struct Clock {
    hours: u8,
    minutes: u8,
}

We use the #[derive] attribute to automatically generate implementations for several useful traits:

  • Copy: Allows instances of Clock to be copied by value, which is efficient for small structs.
  • Drop: Allows Clock instances to go out of scope without needing special cleanup.
  • PartialEq: Generates the logic to compare two Clock instances for equality (e.g., clock1 == clock2). This works by comparing their fields one by one.

Step 2: The Constructor (`new` function) and Normalization Logic

This is the most critical part of our implementation. The constructor must be able to take any combination of hours and minutes—positive, negative, or values outside the normal range—and correctly "normalize" them into a valid 24-hour time.

For example, Clock::new(25, 0) should become 01:00, and Clock::new(0, -90) should become 22:30. The key is to convert everything into a single unit (total minutes), perform the modular arithmetic, and then convert back to hours and minutes.

Here is the ASCII diagram illustrating the normalization flow:

    ● Start
    │
    ▼
  ┌───────────────────────────┐
  │ Input `hours`, `minutes`  │
  │ (as `felt252`)            │
  └───────────┬───────────────┘
              │
              ▼
  ┌───────────────────────────┐
  │ Calculate `total_minutes` │
  │ `(hours * 60) + minutes`  │
  └───────────┬───────────────┘
              │
              ▼
  ┌───────────────────────────┐
  │ Normalize with Modulo     │
  │ `total_minutes % 1440`    │
  │ (Handles rollovers)       │
  └───────────┬───────────────┘
              │
              ▼
  ◆ Is result negative?
   ╱           ╲
  Yes           No
  │              │
  ▼              ▼
[Add 1440]     [Keep as is]
  │              │
  └──────┬───────┘
         ▼
  ┌───────────────────────────┐
  │ `final_minutes`           │
  └───────────┬───────────────┘
              │
              ▼
  ┌───────────────────────────┐
  │ `final_hours = final_minutes / 60` │
  └───────────┬───────────────┘
              │
              ▼
  ┌───────────────────────────┐
  │ `final_minutes = final_minutes % 60` │
  └───────────┬───────────────┘
              │
              ▼
  ┌───────────────────────────┐
  │ Create `Clock` struct   │
  │ with `u8` values        │
  └───────────┬───────────────┘
              │
              ▼
    ● End

And here is the Cairo code that implements this logic:


// Constants for clarity and maintainability
const MINS_PER_HOUR: felt252 = 60;
const MINS_PER_DAY: felt252 = 1440; // 24 * 60

impl ClockImpl of Clock {
    fn new(hours: felt252, minutes: felt252) -> Clock {
        // 1. Convert everything to a total number of minutes.
        let total_minutes = (hours * MINS_PER_HOUR) + minutes;

        // 2. Use the modulo operator to handle rollovers.
        // This finds the remainder when divided by the total minutes in a day.
        let mut rolled_minutes = total_minutes % MINS_PER_DAY;

        // 3. Adjust for negative results from the modulo operation.
        // If `rolled_minutes` is negative, we wrap around from the end of the day.
        if rolled_minutes < 0 {
            rolled_minutes += MINS_PER_DAY;
        }

        // 4. Convert the normalized total minutes back into hours and minutes.
        let final_hours: u8 = (rolled_minutes / MINS_PER_HOUR).try_into().unwrap();
        let final_minutes: u8 = (rolled_minutes % MINS_PER_HOUR).try_into().unwrap();

        Clock { hours: final_hours, minutes: final_minutes }
    }
}

The use of felt252 for the input parameters is crucial. It provides a large range to handle intermediate calculations without overflowing, and it gracefully handles negative numbers, which is essential for our normalization logic.

Step 3: Implementing Arithmetic Traits (`Add` and `Sub`)

To make our clock interactive, we need to add and subtract minutes. We do this by implementing the Add and Sub traits. The logic is simple: we leverage our existing new function, which already knows how to handle normalization.


// Implementation for adding minutes (e.g., `my_clock + 10`)
impl ClockAdd of Add<felt252> for Clock {
    fn add(self: Clock, rhs: felt252) -> Clock {
        // Add the new minutes to the existing ones and let `new` handle the rollover.
        ClockImpl::new(self.hours.into(), self.minutes.into() + rhs)
    }
}

// Implementation for subtracting minutes (e.g., `my_clock - 10`)
impl ClockSub of Sub<felt252> for Clock {
    fn sub(self: Clock, rhs: felt252) -> Clock {
        // Subtract the minutes and let `new` handle the negative result.
        ClockImpl::new(self.hours.into(), self.minutes.into() - rhs)
    }
}

This is a beautiful example of code reuse. Instead of rewriting the complex normalization logic, we simply channel the new values through our robust constructor. This makes the code for add and sub incredibly clean and reliable.

Step 4: Implementing the `Display` Trait for a User-Friendly Output

Finally, we need a way to see the time. The Display trait allows us to define a canonical string representation. We want our clock to print in the "HH:MM" format, with leading zeros for single-digit hours and minutes.


use core::fmt::{Display, Formatter, Result};

impl ClockDisplay of Display<Clock> {
    fn fmt(self: @Clock, ref f: Formatter) -> Result {
        // Format hours with a leading zero if needed
        if *self.hours < 10 {
            f.buffer.append("0");
        }
        f.buffer.append((*self.hours).to_string());
        
        f.buffer.append(":");

        // Format minutes with a leading zero if needed
        if *self.minutes < 10 {
            f.buffer.append("0");
        }
        f.buffer.append((*self.minutes).to_string());

        Result::Ok(())
    }
}

This implementation checks if the hour or minute is less than 10 and prepends a "0" if necessary, ensuring a consistent and professional output like "07:05" instead of "7:5".

The Complete Code

Here is the full, final code for our Clock module, ready to be used in any Starknet project.


use core::ops::{Add, Sub};
use core::fmt::{Display, Formatter, Result};

#[derive(Copy, Drop, PartialEq)]
struct Clock {
    hours: u8,
    minutes: u8,
}

const MINS_PER_HOUR: felt252 = 60;
const MINS_PER_DAY: felt252 = 1440;

impl ClockImpl of Clock {
    /// Creates a new Clock, normalizing hours and minutes.
    /// Handles positive, negative, and out-of-range values.
    fn new(hours: felt252, minutes: felt252) -> Clock {
        let total_minutes = (hours * MINS_PER_HOUR) + minutes;
        let mut rolled_minutes = total_minutes % MINS_PER_DAY;

        if rolled_minutes < 0 {
            rolled_minutes += MINS_PER_DAY;
        }

        let final_hours: u8 = (rolled_minutes / MINS_PER_HOUR).try_into().unwrap();
        let final_minutes: u8 = (rolled_minutes % MINS_PER_HOUR).try_into().unwrap();

        Clock { hours: final_hours, minutes: final_minutes }
    }
}

/// Implements the `+` operator for `Clock` and `felt252`.
impl ClockAdd of Add<felt252> for Clock {
    fn add(self: Clock, rhs: felt252) -> Clock {
        ClockImpl::new(self.hours.into(), self.minutes.into() + rhs)
    }
}

/// Implements the `-` operator for `Clock` and `felt252`.
impl ClockSub of Sub<felt252> for Clock {
    fn sub(self: Clock, rhs: felt252) -> Clock {
        ClockImpl::new(self.hours.into(), self.minutes.into() - rhs)
    }
}

/// Implements the `Display` trait for human-readable output.
impl ClockDisplay of Display<Clock> {
    fn fmt(self: @Clock, ref f: Formatter) -> Result {
        if *self.hours < 10 {
            f.buffer.append("0");
        }
        f.buffer.append((*self.hours).to_string());
        
        f.buffer.append(":");

        if *self.minutes < 10 {
            f.buffer.append("0");
        }
        f.buffer.append((*self.minutes).to_string());

        Result::Ok(())
    }
}

Alternative Approach: Storing Time as Total Minutes

While our struct-based approach with hours and minutes is highly readable, there's an alternative worth considering: storing the time as a single integer representing the total number of minutes past midnight (from 0 to 1439).

Here's a comparison of the two approaches:

    ● Start: Need to represent time
    │
    ├──────────────────────────────────┐
    │                                  │
    ▼                                  ▼
┌──────────────────┐             ┌──────────────────────┐
│ Approach 1: Struct │             │ Approach 2: Single Int │
└─────────┬────────┘             └──────────┬───────────┘
          │                                  │
  `struct Clock {`                  `struct Clock {`
  `  hours: u8,`                     `  total_minutes: u16`
  `  minutes: u8`                   `}`
  `}`                                │
          │                                  │
          ▼                                  ▼
  Pros:                              Pros:
  - Intuitive, readable fields       - Arithmetic is simpler
  - Direct access to H/M             - Potentially more gas-efficient
          │                                  │
          ▼                                  ▼
  Cons:                              Cons:
  - Arithmetic is more complex       - Requires conversion for display
  - Normalization logic needed       - Less intuitive to read state
          │                                  │
    └─────────────────┬────────────────────┘
                      │
                      ▼
                 ● End: Valid time representation

In the single-integer approach, the Clock struct would look like this:


struct Clock {
    minutes_from_midnight: u16, // A u16 can hold values up to 65535, safely covering 0-1439
}

Arithmetic becomes trivial. Adding 10 minutes is just minutes_from_midnight = (minutes_from_midnight + 10) % 1440. However, the trade-off is that whenever you need to display the time or access the hour part, you must perform a conversion:

  • hours = minutes_from_midnight / 60
  • minutes = minutes_from_midnight % 60

This approach can be more gas-efficient for contracts that perform many time calculations but rarely need to display or deconstruct the time. The struct-based approach we implemented is often preferred for its clarity and ease of use, which aligns with the principle of writing safe and understandable smart contracts.


Pros and Cons of Our Clock Implementation

Every design choice has trade-offs. Here's a balanced look at the benefits and limitations of the Clock we built.

Pros Cons / Risks
Type Safety & Readability: Using a dedicated Clock struct makes code intent clear and prevents errors from mixing up raw hour/minute values. No Date or Timezone Handling: This implementation is intentionally simple. It cannot represent a specific date or handle timezone conversions, which would require a much more complex library.
Robust Normalization: The new function correctly handles a wide range of inputs, including negative values, making the component resilient to edge cases. Gas Overhead: While efficient, using a struct and trait implementations has slightly more gas overhead than manipulating raw integers, though this is often a worthwhile trade-off for safety.
Operator Overloading: By implementing Add and Sub, we can use natural operators (+, -), making the code highly intuitive. Dependency on `felt252`: The logic relies on the properties of felt252 for initial calculations. A developer unfamiliar with its behavior (especially with negative numbers) might find the normalization logic non-obvious.
Reusable Component: This clock can be easily packaged into a library and imported into any Starknet project, promoting DRY (Don't Repeat Yourself) principles. Limited Scope: It only handles minute-based arithmetic. Extending it to seconds would require modifying the struct and all associated logic.

Frequently Asked Questions (FAQ)

1. How do I handle time zones with this Clock implementation?

This Clock is timezone-agnostic. It represents a time of day (e.g., 14:30) without context. For blockchain applications, the standard practice is to store all timestamps in Coordinated Universal Time (UTC) to avoid ambiguity. You would handle timezone conversions on the client-side (e.g., in your web frontend) based on the user's local settings.

2. Can I add two `Clock` instances together?

No, and this is by design. Adding two points in time (e.g., 08:00 + 10:30) is a conceptually ambiguous operation. The implementation correctly models the addition of a duration (a number of minutes, represented by felt252) to a point in time (the Clock instance). This prevents logical errors in your code.

3. Why use `felt252` for minutes in the `new` function instead of a smaller integer type?

We use felt252 for the input parameters to provide maximum flexibility and safety during the initial calculation. It allows users to pass in large or negative values (e.g., subtracting 2000 minutes) without causing an integer overflow or underflow. The intermediate total_minutes can become very large or small before it is normalized, and felt252 handles this range effortlessly.

4. What is the purpose of the `Display` trait?

The Display trait provides a standardized way to convert a custom type into a human-readable string. It's essential for logging, debugging, and returning data to frontends. By implementing it, we allow our Clock to be used with formatting macros and print functions, making it a well-behaved component within the Cairo ecosystem.

5. How can I extend this `Clock` to include seconds?

To add seconds, you would:

  1. Add a seconds: u8 field to the Clock struct.
  2. Update the constants: SECS_PER_MIN = 60, SECS_PER_DAY = 86400.
  3. Modify the new function to accept seconds and perform normalization based on total seconds.
  4. Update the Display implementation to format the output as "HH:MM:SS".

6. Is this `Clock` implementation secure for financial applications?

The logic presented is sound for handling 24-hour time arithmetic and has been designed to be robust against rollover errors. However, any code used in a financial smart contract must undergo rigorous testing, formal verification, and a professional security audit. This implementation serves as a strong foundation, but its integration into a larger system requires comprehensive validation.


Conclusion: Mastering Time in a Timeless Ledger

We've successfully constructed a complete, robust, and reusable Clock component in Cairo. More than just solving a specific problem from the Kodikra curriculum, this exercise has illuminated several core principles of Starknet development: the power of custom structs for data modeling, the elegance of traits for defining behavior, and the absolute necessity of rigorous modular arithmetic for handling cyclical operations.

The ability to abstract complex logic into a clean, intuitive interface is the hallmark of a skilled smart contract developer. By building components like this, you create a library of trusted tools that accelerate future development and reduce the surface area for bugs. You've taken a significant step from simply writing code to engineering reliable on-chain systems.

Disclaimer: The code and concepts in this article are based on Cairo syntax and features at the time of writing. The Cairo language and Starknet ecosystem are under active development, and best practices may evolve. Always consult the latest official documentation.

Ready to tackle the next challenge? Continue your journey on the Kodikra Cairo Learning Path to build even more complex and powerful smart contracts. Or, if you want to deepen your understanding of the language, explore more advanced Cairo topics in our complete guide.


Published by Kodikra — Your trusted Cairo learning resource.