E1 - OrderSystem.sol

Implement a smart contract using enums to manage the state of a simple process, like an order system.

Implement a smart contract using enums to manage the state of a simple process, like an order system.

Exercise 1: Enums to Manage Order System State

Objective:

Use enums to manage the state of orders in a simple order system. This system should at least be able to create orders and transition their states through a predefined workflow.

Steps to Approach:

  1. Understand Enums: Enums represent a way to define custom types with a limited set of constant values. For an order system, think about the typical states an order might go through (e.g., Created, Confirmed, Shipped, Delivered).

  2. Define the Enum: Start by defining an enum called OrderState with values that represent possible states of an order.

  3. Struct for Order: Define a struct named Order that includes at least two fields: an identifier (like an id) and the state (using your OrderState enum).

  4. State Transition Functions: Implement functions that change the state of an order. For instance, you might have a function confirmOrder that changes an order's state from Created to Confirmed.

  5. Testing States: Think about how you can test these transitions. What conditions must be met for an order to move to the next state?

Exercise 2: Storing User Profiles with Structs and Mappings

Objective:

Create a contract that utilizes structs to store user profiles and mappings to efficiently look up these profiles by Ethereum addresses.

Steps to Approach:

  1. Understand Structs and Mappings: Structs allow you to create a custom type grouping together several variables, while mappings offer a way to associate keys with values efficiently.

  2. Define the User Profile Struct: Define a struct named UserProfile that includes relevant user information, such as a name and age.

  3. Mapping for Profiles: Create a mapping that associates an Ethereum address with a UserProfile. This setup allows you to quickly retrieve the profile for any given address.

  4. Adding and Retrieving Profiles:

    • Implement a function to add a new user profile. This function should take details like name and age as parameters and store them in the mapping.

    • Implement a function to retrieve a user profile by address. Consider what visibility this function should have.

  5. Handling Edge Cases: Think about how your contract should behave if someone tries to retrieve a profile that doesn't exist or add a profile for an address that already has one.

General Tips for Both Exercises:

  • Start Small: Begin with the basic structure and gradually add more functionality.

  • Test Incrementally: After implementing a piece of functionality, write a test for it. This helps ensure your contract behaves as expected before moving on.

  • Consider Visibility: Decide whether functions should be public, external, internal, or private. For actions like adding or updating information, public or external is often appropriate. For helper functions, internal might be more suitable.

  • Gas Efficiency: Think about the gas implications of your functions. For example, loops and large state changes can be costly.

  • Security: Always keep security in mind. Validate inputs and consider using modifiers for functions that should only be accessible under certain conditions.

By following these steps and focusing on the logic and structure of your contracts, you'll gain a deeper understanding of how to use advanced data types in Solidity to create more complex and functional smart contracts.

OrderSystem.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/* Events */

// Event to log the creation of an order
event OrderCreated(uint orderId, address customer, uint amount);
// Event to log the confirmation of an order
event OrderConfirmed(uint orderId, address customer);
//  Event to log the delivery of an order
event OrderDelivered(uint orderId, address customer);
// Event to log cancelation of an order
event OrderCancelled(uint orderId, address customer);
// Event to log creating a user profile
event ProfileCreated(address user, string name, string age);
// Event to log deleting a user profile
event ProfileDeleted(address user);

contract OrderSystem {
    /* State Variables */
    address private owner;

    // Owner of the contract
    constructor() {
        owner = msg.sender;
    }

    /* Enums */

    // Enums for different states of an order
    enum OrderState {
        Created,
        Confirmed,
        Delivered,
        Cancelled
    }

    /* Structs */

    // Struct to store user profile details and orders
    struct UserProfile {
        string name;
        string age;
        uint[] currentOrders;
        uint[] completedOrders;
    }

    // Struct to store order details and state
    struct Order {
        uint id;
        address customer;
        uint amount;
        OrderState state;
    }

    /* Mappings */

    // mapping to store user profiles using address as key
    mapping(address => UserProfile) private profiles;

    // mapping to store orders, using the orderId as key
    mapping(uint => Order) private orders;

    // variable to store order id
    uint private orderId = 0;

    /* Modifiers */

    // Modifier to check if the sender is the owner
    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call this function");
        _;
    }

    /* Functions */

    /**
     * @dev Set user profile. Must be called before placing an order
     * @param _name Name of the user
     * @param _age Age of the user
     */
    function newUserProfile(string memory _name, string memory _age) public {
        // Check if the name and age are not empty
        require(bytes(_name).length > 0, "Name cannot be empty");
        require(bytes(_age).length > 0, "Age cannot be empty");
        // Set the user profile details
        profiles[msg.sender].name = _name;
        profiles[msg.sender].age = _age;

        emit ProfileCreated(msg.sender, _name, _age);
    }

    /**
     * @dev Create an order
     * @param _customer Address of the customer
     * @param _amount Amount of the order
     * @return orderId of the created order
     */
    function createOrder(
        address _customer,
        uint _amount
    ) public returns (uint) {
        // Check if the amount is greater than 0
        require(_amount > 0, "Amount should be greater than 0");

        // Must create a user profile before placing an order
        require(
            bytes(profiles[_customer].name).length > 0,
            "User profile does not exist"
        );

        // Sets the orderId to the created order then increments OrderId
        uint currentOrderId = orderId;
        orderId++;

        // Add the order to the customer's profile
        UserProfile storage profile = profiles[_customer];
        // Add the order to the customer's current orders
        profile.currentOrders.push(currentOrderId);
        // Add the order to the orders mapping
        orders[currentOrderId] = Order(
            currentOrderId,
            _customer,
            _amount,
            OrderState.Created
        );
    
        emit OrderCreated(currentOrderId, _customer, _amount);

        return currentOrderId;
    }

    /**
     * @dev Confirm an order
     * @param OrderToBeConfirmed Order to be confirmed
     */
    function confirmOrder(uint OrderToBeConfirmed) public {
        // Only customer can confirm the order
        require(
            msg.sender == orders[OrderToBeConfirmed].customer,
            "Only customer can confirm order"
        );
        // Get the order to be confirmed
        Order storage order = orders[OrderToBeConfirmed];
        // Check if the order is in the created state
        require(order.state == OrderState.Created, "Order already confirmed");

        order.state = OrderState.Confirmed;

        emit OrderConfirmed(OrderToBeConfirmed, order.customer);
    }

    /**
     * @dev Deliver an order
     * @param deliveredOrder Order to be delivered
     */
    function confirmDelivery(uint deliveredOrder) public {
        // require that the msg.sender is the user who created the order
        require(
            msg.sender == orders[deliveredOrder].customer,
            "Only customer can confirm delivery"
        );
        // Get the order to be delivered
        Order storage order = orders[deliveredOrder];
        // Get the customer's profile
        UserProfile storage profile = profiles[order.customer];
        // Check if the order is not already delivered
        require(order.state != OrderState.Delivered, "Order already delivered");
        // Check if the order is in the confirmed state
        require(order.state == OrderState.Confirmed, "Order not confirmed");

        order.state = OrderState.Delivered;

        emit OrderDelivered(deliveredOrder, order.customer);

        // remove from current orders and add to completed orders
        for (uint i = 0; i < profile.currentOrders.length; i++) {
            if (profile.currentOrders[i] == deliveredOrder) {
                profile.currentOrders[i] = profile.currentOrders[
                    profile.currentOrders.length - 1
                ];
                profile.currentOrders.pop();
                break;
            }
        }

        // Add the order to the customer's completed orders
        profile.completedOrders.push(deliveredOrder);
    }

    function adminDeleteProfileAndOrders(address user) public onlyOwner {
        require(
            bytes(profiles[user].name).length > 0,
            "Profile does not exist for the given address"
        );
        // Cancel all orders of user
        for (uint i = 0; i < profiles[user].currentOrders.length; i++) {
            cancelOrder(profiles[user].currentOrders[i]);
        }
        // Delete the user profile
        delete profiles[user];
        // Emit the event
        emit ProfileDeleted(user);
    }

    /**
     * @dev Delete user profile
     */
    function deleteProfile() public {
        // Check if the profile exists
        require(
            bytes(profiles[msg.sender].name).length > 0,
            "Profile does not exist for the given address"
        );
        require(profiles[msg.sender].currentOrders.length == 0, "Cannot delete profile with active orders");
        // Delete the user profile
        delete profiles[msg.sender];
        // Emit the event
        emit ProfileDeleted(msg.sender);
    }

    /**
     * @dev Cancel an order
     * @param idToCancel Order to be cancelled
     */
    function cancelOrder(uint idToCancel) public {
        // require that the msg.sender is the user who created the order
        if(msg.sender != owner){
            require(
                msg.sender == orders[idToCancel].customer,
                "Only customer can cancel order"
            );
        }
       
        // Get the order to be delivered
        Order storage order = orders[idToCancel];
        // Get the customer's profile
        UserProfile storage profile = profiles[order.customer];

        // Check if the order is not already cancelled
        if (order.state == OrderState.Cancelled) {
            revert("Order already cancelled");
        }

        // Check if the order is not already delivered
        require(order.state != OrderState.Delivered, "Order already delivered");
        // Check if the order is in the confirmed state
        require(order.state == OrderState.Confirmed, "Order not confirmed");

        order.state = OrderState.Cancelled;

        emit OrderCancelled(idToCancel, order.customer);

        // remove from current orders
        for (uint i = 0; i < profile.currentOrders.length; i++) {
            if (profile.currentOrders[i] == idToCancel) {
                profile.currentOrders[i] = profile.currentOrders[
                    profile.currentOrders.length - 1
                ];
                profile.currentOrders.pop();
                break;
            }
        }

    }

    
    /* Getter Functions */

    /**
     * @dev Get user profile
     * @param _user Address of the user
     * @return Name and age of the user
     */
    function getProfile(
        address _user
    ) public view onlyOwner returns (string memory, string memory) {
        // Check if the address is valid
        require(_user != address(0), "Invalid address");
        // Check if the profile exists
        require(
            bytes(profiles[_user].name).length > 0,
            "Profile does not exist for the given address"
        );

        return (profiles[_user].name, profiles[_user].age);
    }

    /**
     * @dev Get order state
     * @param id Order id
     * @return Order state
     */
    function getOrderState(uint id) public view returns (OrderState) {
        return orders[id].state;
    }

    /**
     * @dev Get order details
     * @param id Order id
     * @return Order details
     */
    function getOrder(uint id) public view returns (Order memory) {
        require(orders[id].id == id, "Order does not exist");

        return orders[id];
    }

    /**
     * @dev Get orders of a user
     * @param _user Address of the user
     * @return Orders of the user
     */
    function getOrders(address _user) public view onlyOwner returns (uint[] memory) {
        require(
            bytes(profiles[_user].name).length > 0,
            "Profile does not exist for the given address"
        );
        return profiles[_user].currentOrders;
    }

    function getMyOrders() public view returns (uint[] memory) {
        require(
            bytes(profiles[msg.sender].name).length > 0,
            "Profile does not exist for the given address");

        return profiles[msg.sender].currentOrders;
    }
}

Feedback

User

Please give me some feedback on my OrderSystem.sol smart contract. Does it cover all the bases of exercise 1 and 2? {paste smart contract here}

ChatGpt

Your OrderSystem.sol smart contract demonstrates a comprehensive understanding of Solidity concepts, including state variables, enums, structs, mappings, events, and function modifiers. Here's feedback on its structure, functionality, and adherence to best practices, focusing on whether it covers the bases for exercises 1 and 2 from Lesson 5:

Strengths:

  1. Use of Enums: Effectively uses enums to represent the state of an order, showcasing understanding of enumerated types for tracking order progress.

  2. Structs and Mappings: Utilizes structs to aggregate user and order information and mappings to associate data with specific users and orders. This demonstrates a good grasp of advanced data types and their application.

  3. Event Logging: Implements events for significant actions within the contract (order creation, confirmation, delivery, and user profile creation), enabling off-chain applications to listen and react to these contract events.

  4. Modifiers for Access Control: Includes a onlyOwner modifier to restrict certain actions to the contract owner, showcasing an understanding of function modifiers for access control.

Areas for Improvement:

  1. Profile Creation Before Order: The contract requires a user profile to exist before an order can be created. This dependency isn't inherently problematic but could be highlighted in documentation or through a function that explicitly handles new user onboarding.

  2. Gas Optimization: Looping through currentOrders to update order status (from current to completed) can become gas-intensive with a large dataset. Consider alternatives, such as mapping orders to their status for more efficient updates.

  3. Error Handling: The contract does well in using require for validations. However, consider utilizing custom error messages (via Solidity 0.8.x custom errors) to reduce gas costs further and improve error clarity.

  4. Visibility and Function Organization: The contract's structure is logical, but grouping public view functions and state-changing functions together could improve readability. Additionally, explicitly stating function visibility (even though public is the default) enhances clarity.

Additional Recommendations:

  • Profile Deletion and Order Cancellation: Consider adding functionality to handle user profile deletion and order cancellation, including state changes and event emissions for these actions.

  • Contract Upgradeability: As the contract evolves, consider how you might upgrade it or handle versioning, especially concerning user data and order history.

  • Security Considerations: Review and test for common security vulnerabilities in smart contracts, such as reentrancy, overflow/underflow, and denial of service. Tools like Slither or Mythril can assist in static analysis for vulnerabilities.

Conclusion:

Your OrderSystem.sol contract is well-designed and aligns with the objectives of exercises 1 and 2 from Lesson 5, effectively utilizing advanced Solidity concepts. As you refine this contract, focus on optimization, security, and potentially incorporating feedback mechanisms or dispute resolution to handle real-world complexities in order processing systems. Great work on building a solid foundation for a decentralized order management system!

Last updated