Celo Gas Optimization Strategies with Solidity Assembly to Improve Smart Contract Development Efficiency

Celo Gas Optimization Strategies with Solidity Assembly to Improve Smart Contract Development Efficiency https://celo.academy/uploads/default/optimized/2X/7/7f3d9874617181c24386e3ba79fa7719f0d077e9_2_1024x576.png
none 0.0 0

Introduction

Efficiency is still essential when developing smart contracts in the dynamic blockchain world, particularly in the peculiar setting of the Celo blockchain. An in-depth discussion of Solidity Assembly’s role in optimizing gas consumption—a crucial component of blockchain interactions—is provided in this article. It reveals a range of methods for performance improvement, from investigating inline assembly peculiarities to creating gas-efficient data structures and putting in place efficient algorithms. This article offers the necessary information to overcome development difficulties and fully take advantage of smart contract optimization on the Celo network, regardless of your level of blockchain expertise.

Prerequisites

Prior to using Solidity Assembly for gas optimization, it’s crucial to be knowledgeable about:

  • Programming language Solidity.
  • Smart contracts and how they are used.
  • Ethereum and its gas computation system.
  • Basic knowledge of how the Ethereum Virtual Machine (EVM) works.

Requirements

To put these strategies into practice, you would require:

  • Solidity compiler: You have the option of installing the Solidity compiler on your computer or using Remix, an online Solidity IDE.
  • Celo test network: Celo Alfajores

Understanding Gas and Ethereum’s Importance

It is crucial to comprehend what “gas” means in the context of Celo before delving into Solidity Assembly and its efficiency techniques. Simply explained, gas is a unit used to represent the amount of processing needed to complete a given operation on the Celo network or other EVM-compatible network. Every action, from straightforward transactions to intricate smart contract interactions, needs a specific quantity of gas to be successful. In order to lower transaction costs and improve smart contract performance, gas utilization must be optimized.

The Need for Assembly-Level Optimization and Solidity

The most popular language for creating smart contracts on Ethereum is called Solidity. It is a high-level language that offers a simple syntax, simplifying the creation and deployment of smart contracts for developers.

Solidity abstracts away a lot of the difficulties that come with low-level languages, yet this convenience can result in inefficient gas utilization. To fix this, programmers can use Solidity Assembly, a low-level language that provides direct access to operations on the Ethereum Virtual Machine (EVM). The fine-grained optimization made possible by this degree of control has the potential to drastically lower gas use.

Step 1: Environment Setup and smart contract development

The first step is to create an environment for our smart contract development. For simplicity, let’s use Remix IDE as our development environment. In the contracts directory on your REMIX IDE, create a new file called Solidity.sol and paste the following code:

Solidity.sol

// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract Solidity {
    uint256 public sumOfNumbers;
    struct Employee {
        address token;
        uint128 salary;
        uint64 nextPayTimestamp;
        uint64 timePeriod;
    }
    mapping(address => Employee) public employeeDetails;
    bool public paused; // slot 1

    event PlanPaused(bool);

    function sumPure(
        uint256[] memory _data
    ) public pure returns (uint256 result) {
        for (uint256 i; i < _data.length; ++i) {
            result += _data[i];
        }
    }

    function sum(uint256[] memory _data) public {
        uint256 result;
        uint256 len = _data.length;
        for (uint256 i; i < len; ++i) {
            result += _data[i];
        }
        sumOfNumbers = result;
    }

    function pause() external {
        paused = true;
        emit PlanPaused(true);
    }

    function addEmployees(
        address _employeeAddress,
        address _token,
        uint128 _employeeSalary,
        uint64 _firstPayTimestamp,
        uint64 _timePeriod
    ) external {
        Employee storage emp = employeeDetails[_employeeAddress];
        emp.salary = _employeeSalary;
        emp.token = _token;
        emp.nextPayTimestamp = _firstPayTimestamp;
        emp.timePeriod = _timePeriod;
    }

    function editEmployees(
        address _employeeAddress,
        address _token,
        uint128 _employeeSalary,
        uint64 _nextPayTimestamp,
        uint64 _timePeriod
    ) external {
        Employee storage emp = employeeDetails[_employeeAddress];
        emp.salary = _employeeSalary;
        emp.token = _token;
        emp.nextPayTimestamp = _nextPayTimestamp;
        emp.timePeriod = _timePeriod;
    }

    function withdrawToken(address _tokenAddress, uint256 _amount) external {
        IERC20(_tokenAddress).transfer(msg.sender, _amount);
    }
}

This contract is a straightforward personnel management system with some extra features. The contract is broken into as follows:

State Variables
The contract includes the following state variables:

  • sumOfNumbers is a public variable that stores the total of a set of numbers that were passed to a particular function.
  • Employee struct: This data structure contains all of an employee’s crucial information. It includes the timing for the next payment, the employee’s wage, the token address for the payment method, and the frequency of payments.
  • employeeDetails: This mapping establishes a connection between an employee’s Ethereum address and the details that go with it by pairing the employee’s address with the relevant Employee struct.
  • paused: This boolean variable represents the system’s functioning status. If this is the case, the system is inactive or halted.

Events
A second event, PlanPaused, is also declared in the contract. Events enable light clients to effectively respond to changes. When the pause function is used, the PlanPaused event is generated, informing any listeners that the system has been paused.

Functions
To interact with the state variables and implement the intended functionality of the contract, several functions are developed.

  • sumPure adds the elements of an array of unsigned integers (uint256) and returns the result. This is a pure function, which means it doesn’t alter or even read the state of the contract.
  • sum: In addition to updating the sumOfNumbers state variable to hold the sum of the input numbers, this function, which is similar to sumPure, modifies the sumOfNumbers state variable
  • pause: This function’s purpose is to stop the contract’s operations. It causes the PlanPaused event to be raised and sets the paused state variable to true.
  • addEmployees: With the help of the addEmployees function, a new employee’s information can be added to the employeeDetails mapping.
  • editEmployees: Editing an existing employee’s details in the employeeDetails mapping is possible with the help of the function editEmployees.
  • WithdrawToken: This feature enables the withdrawal of a certain quantity of an ERC20 token. It makes use of the IERC20 interface’s transfer technique.

For the IERC20 interface, this contract makes use of the OpenZeppelin contracts library. A reputable library called OpenZeppelin provides a selection of safe and peer-reviewed smart contracts.

Step 2: Gas Optimization Techniques Using Solidity Assembly

Next, in the same contracts directory, create a new file called Assembly.sol which would contain a gas-optimized version of our previous smart contract.

Assembly.sol

// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;

contract Assembly {
    uint256 public sumOfNumbers; // slot 0
    struct Employee {
        address token;
        uint128 salary;
        uint64 nextPayTimestamp;
        uint64 timePeriod;
    }
    mapping(address => Employee) public employeeDetails;
    bool public paused; // slot 1

    event PlanPaused(bool);

    function sumPure(
        uint256[] memory _data
    ) public pure returns (uint256 result) {
        assembly {
            // because array is in memory we use mload, for calldata CALLDATALOAD
            // yul interprents the _data array as a memory address / 32B word
            let len := mload(_data)
            // 0x20 refers to 32 bytes
            let data := add(_data, 0x20)
            for {
                let end := add(data, mul(len, 0x20))
            } lt(data, end) {
                data := add(data, 0x20)
            } {
                result := add(result, mload(data))
            }
        }
    }

    function sum(uint256[] memory _data) public {
        assembly {
            let len := mload(_data)
            let data := add(_data, 0x20)
            let result
            for {
                let end := add(data, mul(len, 0x20))
            } lt(data, end) {
                data := add(data, 0x20)
            } {
                result := add(result, mload(data))
            }
            sstore(0, result)
        }
    }

    function pause() external {
        assembly {
            sstore(paused.slot, true)
            // emitting event
            mstore(0x80, true)
            // the hash is just the event PlanPaused hashed
            log1(
                0x80,
                0x01,
                0x3b52531264dffb2eb5a1cb50b4adb7d62109b880fd7615400f7d32fc1bb315a2
            )
        }
    }

    // slot x - [token]
    // slot x+1 - [salary,nextPayTimestamp,timePeriod]
    function addEmployees(
        address _employeeAddress,
        address _token,
        uint128 _employeeSalary,
        uint64 _firstPayTimestamp,
        uint64 _timePeriod
    ) external {
        assembly {
            // Calculating which slot to be stored in
            mstore(0x00, _employeeAddress)
            mstore(0x20, employeeDetails.slot)
            // generate unique storage slot
            let slot := keccak256(0, 0x40)
            let w := sload(slot)
            // Clearing 20 bytes and loading token address
            w := and(w, not(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))
            w := or(w, and(_token, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))
            sstore(slot, w)

            // Clearing and loading the first 16 bytes
            let s := sload(add(slot, 1))
            s := and(s, not(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))
            s := or(s, and(_employeeSalary, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))

            // // clearing and loading middle 8 bytes
            // s := and(s,0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
            s := and(s, not(shl(128, 0xFFFFFFFFFFFFFFFF)))
            s := or(s, shl(128, and(_firstPayTimestamp, 0xFFFFFFFFFFFFFFFF)))

            // // clearing and loading last 8 bytes
            s := and(s, not(shl(192, 0xFFFFFFFFFFFFFFFF)))
            s := or(s, shl(192, and(_timePeriod, 0xFFFFFFFFFFFFFFFF)))

            sstore(add(slot, 1), s)
        }
    }

    function editEmployees(
        address _employeeAddress,
        address _token,
        uint128 _employeeSalary,
        uint64 _nextPayTimestamp,
        uint64 _timePeriod
    ) external {
        assembly {
            // Calculating which slot to be stored in
            mstore(0x00, _employeeAddress)
            mstore(0x20, employeeDetails.slot)
            let slot := keccak256(0, 0x40)
            let w := sload(slot)
            // Clearing 20 bytes and loading token address
            w := and(w, not(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))
            w := or(w, and(_token, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))
            sstore(slot, w)

            // Clearing and loading the first 16 bytes
            let s := sload(add(slot, 1))
            s := and(s, not(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))
            s := or(s, and(_employeeSalary, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))

            // // clearing and loading middle 8 bytes
            // s := and(s,0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
            s := and(s, not(shl(128, 0xFFFFFFFFFFFFFFFF)))
            s := or(s, shl(128, and(_nextPayTimestamp, 0xFFFFFFFFFFFFFFFF)))

            // // clearing and loading last 8 bytes
            s := and(s, not(shl(192, 0xFFFFFFFFFFFFFFFF)))
            s := or(s, shl(192, and(_timePeriod, 0xFFFFFFFFFFFFFFFF)))

            sstore(add(slot, 1), s)
        }
    }

    function withdrawToken(address _tokenAddress, uint256 _amount) external {
        bytes4 sig = 0xa9059cbb;
        assembly {
            let data := mload(0x40)
            mstore(data, sig)
            mstore(add(data, 0x04), caller())
            mstore(add(data, 0x24), _amount)
            let result := call(
                2000000,
                _tokenAddress,
                0,
                data,
                0x52,
                data,
                0x01
            )
            switch result
            case 0 {
                revert(0, 0)
            }
            default {

            }
        }
    }
}

This contract uses inline assembly to increase control and efficiency over the prior Solidity smart contract you gave and is a gas-optimized version of that contract. An explanation of the contract, concentrating on the building blocks, is provided below:

  • Function sumPure
    The sumPure function uses assembly to add up all the numbers in a memory-based array. Here, assembly is being used since reading straight from memory can be less expensive than accessing variables through Solidity. The array data is accessed by computing the position in memory (add(_data, 0x20), as arrays have their length at the first 32-byte slot), and the array length is loaded using mload(_data). The data is then iterated over in a loop, adding each value to the final result.

  • sum Function

    In addition to storing the outcome in the sumOfNumbers state variable at slot 0 of the contract’s storage (sstore(0, result)), the sum function is comparable to sumPure.

  • the pause Function
    The system is paused using the pause function, which also utilizes assembly to send the PlanPaused event. It creates a log1 call (which corresponds to emitting an event) and saves true in the paused state variable slot of the state machine. The log1 arguments specify the topic (which is the hashed event signature) and the data to log, which in this case is true. In high-level Solidity, calling emit is less effective than doing this.

  • function addEmployees and function editEmployees
    The employeeDetails mapping can be updated or expanded with the use of the addEmployees and editEmployees functions. They efficiently cram the data of each employee into two 32-byte storage slots using assembly to create a one-of-a-kind storage slot for each employee. By shifting bits, the shl instruction effectively positions each piece of data in the slot at the proper location. This densely packed construction uses less storage, which helps to conserve gas.

  • the withdrawToken feature
    To transfer tokens, the withdrawToken method uses assembly to make a call to an ERC20 token contract. In many cases, this inline assembly approach uses less gas than communicating with contracts via Solidity’s high-level syntax. The call guarantees atomicity by reverting the transaction in the event of failure.

Inline assembly is used in this contract to demonstrate a number of methods for reducing gas prices in Solidity smart contracts. These methods enable more cost-effective interaction with other contracts, more effective memory and storage consumption, and more direct control over Ethereum’s EVM. They should only be used sparingly because they complicate the EVM and make it more difficult to comprehend and reason about the code.

Finally, compile and deploy both the Solidity.sol and Assembly.sol smart contracts and compare the gas used by the addEmployees functions that both contracts consumed while being executed. You can go through this article for detailed steps on how to deploy smart contracts to the Celo network using the Remix IDE.

From the two images above, we can see that our addEmployees function in Solidity.sol smart contract used a gas of 78901 but that of the assembly gas-optimized version of it, Assembly.sol used a gas of 78187 which is way lesser.

Conclusion

The creation of successful and economical smart contracts requires the optimization of gas. Developers can use Solidity Assembly to communicate with the Celo blockchain directly and use techniques that increase the gas efficiency of their smart contracts. It’s crucial to remember that while assembly provides more control, it also necessitates precise attention to detail in order to prevent errors and security flaws.

Next Steps

  • Start incorporating these strategies into your smart contracts and monitor the change in gas consumption. You may write, test, and deploy your contracts on the Celo network using Remix, an online IDE for Solidity.
  • Make an effort to rework and optimize current contracts to increase their gas efficiency. For Celo development, tools like Ganache can offer a private blockchain where you may test your contracts.
  • Continue to research other approaches and tactics for gas optimization. A plethora of material is available on websites like Solidity Docs and ConsenSys Best Practices.
  • Learn about and comprehend EVM opcode prices to help you choose better while optimizing your code. Information on gas prices and EVM operations may be found in depth in the Ethereum Yellow Paper and other Etherscan resources.

About the Author

Israel Okunaya is an ace writer with a flair for simplifying complexities and a knack for storytelling. He leverages over four years of experience to meet the most demanding writing needs in different niches, especially food and travel, blockchain, and marketing. He sees blockchain as a fascinating yet tricky affair. So, he is given to simplifying its complexities with text and video tutorials.

References

  1. Solidity Documentation: Assembly
  2. Ethereum Yellow Paper: Ethereum: A Secure Decentralised Generalised Transaction Ledger
  3. ConsenSys: Ethereum Smart Contract Best Practices
  4. Etherscan: Ethereum Gas Chart
  5. Celoscan
  6. Remix Codebase
  7. Github Repo
10 Likes

I love this concept…looking forward to how you would go about it. Hopefully it gets approved.

4 Likes

Fantastic news! Your proposal has landed in this week’s top voted list. As you begin your project journey, remember to align with our community and technical guidelines, ensuring a high quality platform for our developers. Congratulations! :mortar_board: :seedling:

5 Likes

i’ll be reviewing this in 1-2 days @Southpaw

3 Likes

Hi I’ll be reviewing your piece in 1 to 2 days @Southpaw

3 Likes

For swiftness and better focus, I would love @Phenzic to review this particular tutorial while you handle the other piece.
I hope that is fine, @4undRaiser.

2 Likes

@Southpaw as long as i am not breaking any rules. I’ll be reviewing the tutorial as i stated before. you can tag the moderators @ishan.pathak2711 if there’s a problem. thanks

2 Likes

I don’t think you can review a tutorial against the consent of the author. I even had to explain to you in the DMs. Making this an argument is not best honestly. Why the insistence though? It is well.

2 Likes

Hi @4undRaiser you can continue with reviewing this piece :clinking_glasses:

3 Likes

Yeah. Will be looking forward to what the next steps will be, @4undRaiser. Thanks for understanding @Phenzic

1 Like

@4undRaiser Any news here?

1 Like

Greate job @Southpaw
you used remix for testing but you specified ganache in the prerequisite. can you fix that

1 Like

Also

Also can you explain more which function you called in this comparison.

and add the github repository for your code. Thanks

@Southpaw

2 Likes

Fixed all the corrections required. Kindly check and revert.

1 Like

okay. Just remaining the github repository @Southpaw not the remix

1 Like

I didn’t use GitHub. I used remix. I believe the rule requires including your code base. It doesn’t stipulate Github necessarily.
My codebase in this case was Remix. I hope you get to understand me.

2 Likes

@Southpaw I understand you . But github is a more convenient way to host your code, For quality purposes. Try it and let me know wah you think.

1 Like

I just added a Github repo link. You can check now.

1 Like

good you can move to publish now

2 Likes

Your tutorial on ‘Celo Gas Optimization Strategies with Solidity Assembly to Improve Smart Contract Development Efficiency’ is an outstanding guide for developers seeking to optimize gas usage and enhance the efficiency of their smart contract development on the Celo blockchain. Great work bro!

2 Likes