Solidity Design Patterns to Save Gas on Blockchain External Transactions

5 Gas Optimization Strategies for Ethereum-based Smart Contract Architectures

save gas - solidity design patterns

save gas - solidity design patterns

In this article, I will discuss design patterns and architectures to help you optimize smart contracts to save gas on transactions, offering less costly gas operations for your smart contract users.

If we apply design patterns to our application’s architecture, we can better align it with the unique properties of an EVM blockchain. We collected these patterns based on multiple references in my code lab, and have tested them using sample contracts. In this article, we will look at patterns used for External Transactions. This category focuses on creating contracts and sending transactions from an EOA (Externally Owned Address).

Proxy

One challenge with smart contracts is their immutability. When updates or changes are necessary, deploying new contracts and updating related contracts due to bugs or extensions can be expensive. This is when The Proxy Upgrade pattern comes into play.

Proxy patterns involve a set of smart contracts working together to facilitate upgrading other smart contracts while preserving their immutability. Here’s how it works: a Proxy contract holds the addresses of referred smart contracts in its state variables, and the contract owner can later change the stored addresses to updated versions. Consequently, only the references to the new smart contract need to be updated. This approach allows the owner to maintain contracts efficiently and, in the process, save gas by reducing the cost of deploying new ones.

Here are some practical used cases and examples:

Aragon Contracts for Upgradability (ERC-897 implementation)

We can also view instances of the Delegate Proxy pattern in Aragon’s AppProxyUpgradable, AppProxyPinned, and KernelProxy contracts.

It’s becoming increasingly popular to employ proxies that delegate their internal logic as a technique for smart contract upgradeability and the economical creation of duplicate contracts to separate contracts.

Clones by OpenZepplin (ERC-1167 implementation)

There have been community-vetted implementations of the Proxy Delegation pattern in projects like OpenZeppelin, a widely used framework for building secure smart contracts. By referring to OpenZeppelin’s usage of this pattern, developers can gain insights into effective implementation and best practices.

When using a proxy pattern, there is a chance that the newly deployed contracts get conflicts when interacting with storage slots of the implementation contract behind the proxy. This issue might happen if the owner mistakenly changes the variables at some point when upgrading or changing the proxy. To evade this issue, we can use EIP-1967, which helps by keeping a standardized location where proxies store the address of the logic contract, along with other proxy-specific details to which they delegate. `EIP-1967` is implemented in OpenZepplin’s Clone by default.

Diamonds Multi-facet Proxy (ERC-2535)

When it comes to this new improvement proposal that plans to blow minds, the Diamond Proxy pattern shines. This architectural structure helps you create modular smart contracts that you can extend after deployment. It helps evade the contract size of 24kb, provides a solution for organizing the contract code & data, adds to upgradability features, and more. 

The only current drawback is since this is a newly introduced ERC, platforms like Etherscan have not yet kept up with the ability to access the functions and read through its structure. Regardless, it’s a fully working solution that works nicely on the EVM contracts. 

Data Contract

When designing smart contracts, it’s crucial to consider the specific requirements of your application and weigh the trade-offs between gas efficiency, modularity, and upgradability based on your use case.

In this pattern, we store the data in a dedicated smart contract. The Data contract is then accessed by one or more contracts responsible for processing and implementing the logic. It provides you more flexibility to update the processing logic if required. That means only the processing contract needs redeployment, while the data remains in the Data Contract. This approach enhances the efficiency of updating and maintaining the smart contract.

When you separate data and logic into different contracts, you introduce inter-contract communication, which involves additional gas expenditures. Despite the additional costs associated with external calls, the Data Contract pattern can be gas-efficient where large datasets and modularity are the priority. Let’s look at the benefits here.

1) Optimizing Storage Costs

The cost savings primarily come from optimizing storage-related gas costs. Updating data in a separate contract can be more gas-efficient than updating data within the same contract, especially when dealing with large datasets.

2) Modularity and Upgradability

The separation of data and logic allows for modular upgrades. While external calls have associated costs, this approach provides a trade-off between gas efficiency and the flexibility to upgrade logic independently of the data.

Below is a simplified example in Solidity to illustrate the Data Contract pattern. In this example, I’ll create a simple Data Contract to store and manage user data separately from the processing logic contract.

The DataContract.solcontract holds the data and is responsible for managing it.

Solidity
// DataContract.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract DataContract {
    // Define data structure
    struct UserData {
        uint256 userId;
        string userName;
        uint256 userBalance;
    }

    // Mapping to store user data
    mapping(address => UserData) public usersData;

    // Function to set user data
    function setUserData(address userAddress, uint256 id, string memory name, uint256 balance) external {
        usersData[userAddress] = UserData(id, name, balance);
    }

    // Function to get user data
    function getUserData(address userAddress) external view returns (UserData memory) {
        return usersData[userAddress];
    }
}

Now, let’s create a Processing Contract that interacts with the Data Contract and implements some logic. For simplicity, I’ll create a function to update a user’s balance.

In this example, the ProcessingContract interacts with the DataContract to update a user’s balance without having to duplicate the entire data to a new smart contract. Remember to deploy the DataContract first and then deploy the ProcessingContract with a reference to the deployed DataContract.

Solidity
// ProcessingContract.sol
// This contract contains the processing logic and interacts with the Data Contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./DataContract.sol"; // Import the Data Contract

contract ProcessingContract {
    DataContract public dataContract; // Reference to the Data Contract

    constructor(DataContract _dataContract) {
        dataContract = _dataContract;
    }

    // Function to update user balance
    function updateUserBalance(uint256 newBalance) external {
        // Access user data from Data Contract
        DataContract.UserData memory userData = dataContract.getUserData(msg.sender);

        // Update balance
        userData.userBalance = newBalance;

        // Update user data in Data Contract
        dataContract.setUserData(msg.sender, userData.userId, userData.userName, userData.userBalance);
    }
}

Implementation with Proxies

The Data Contract pattern is often integrated into implementations of the Proxy pattern. By combining these patterns, you achieve a modular and cost-effective solution for data storage and logic execution.

Event Logs

Event Logs are a mechanism for emitting information from a smart contract, and they are often used for off-chain communication or to notify other contracts about specific actions.

Think of events in smart contracts as the blockbuster movie trailers of the blockchain. They give you a sneak peek into the epic plot twists and exciting developments in the system. Now, imagine if we crammed every thrilling trailer directly into the blockchain – it’d be like buying a ticket for every trailer, and we all know that’s a costly affair!

But hold on, If the world beyond the blockchain wants to catch up on the blockchain’s blockbuster history, they can tap into the Event Log. It’s like having a special behind-the-scenes tour!

So, using Event Logs in a smart contract on Ethereum can contribute to gas savings in various ways. Here are some ways in which Event Logs can help save gas:

  1. Off-Chain Data Retrieval:
    • Event Logs provide a way to emit information that can be observed off-chain. This allows clients or external systems to listen for events and retrieve relevant data without the need for on-chain transactions.
    • By offloading certain data retrieval tasks to off-chain components, you can reduce the need for on-chain computations and, consequently, save gas.
  2. Decoupling Smart Contract Components:
    • When different components of a decentralized application (DApp) need to communicate, Event Logs provide a decoupled and efficient mechanism. Instead of direct contract-to-contract calls, contracts can emit events that other contracts or off-chain components can listen to.
    • This decoupling can result in modular and more maintainable smart contract architectures. Gas savings come from reducing the complexity and cost of direct contract interactions.
  3. Frontend Interaction:
    • Event Logs are often used to notify the frontend of a DApp about specific on-chain events. This can help update the user interface without the need for constant polling of the blockchain.
    • Gas is saved by reducing the need for continuous queries from the frontend to the blockchain, as the frontend can react to specific events emitted by the smart contract.
  4. Reduced Storage Costs:
    • Emitting events does not incur significant storage costs on the Ethereum blockchain. Storing data directly in the contract state can be more expensive in terms of gas.
    • By emitting events instead of storing certain information on-chain, you can achieve cost savings, especially when the data is only needed for off-chain purposes.
  5. Gas Refunds for Failed Transactions:
    • In Ethereum, if a transaction fails, any gas spent before the failure is refunded. Emitting events early in a transaction allows for gas refunds in case the subsequent logic encounters an issue.
    • This can be a defensive programming strategy that minimizes the gas spent in the case of unexpected errors.

Here’s a simple example in Solidity to illustrate the use of Event Logs:

Solidity
// EventLoggingContract.sol
// This contract emits an event when a new item is added.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract EventLoggingContract {
    event ItemAdded(address indexed sender, string itemName);

    function addItem(string memory itemName) external {
        // Perform some logic...
        
        // Emit an event to notify about the new item
        emit ItemAdded(msg.sender, itemName);
    }
}

By leveraging Event Logs strategically, you can enhance the efficiency of your smart contracts and reduce the overall gas costs associated with certain operations, while accessing the logged information off-chain. Now, let’s imagine an off-chain JavaScript code snippet used Ethers.js to listen for and retrieve events.

JavaScript
const { ethers } = require('ethers');

async function retrieveEvents() {
    // Connect to Ethereum
    const provider = new ethers.providers.JsonRpcProvider('YOUR_ETHEREUM_NODE_URL'); // Replace with your Ethereum node URL

    // Replace with the actual contract address and ABI
    const contractAddress = 'YOUR_CONTRACT_ADDRESS';
    const contractAbi = [ /* Replace with the actual ABI of your contract */ ];

    // Connect to the contract
    const exampleContract = new ethers.Contract(contractAddress, contractAbi, provider);

    // Subscribe to the ItemAdded event
    exampleContract.on('ItemAdded', (sender, itemName, event) => {
        console.log('New item added:', itemName);
        console.log('Event emitted by:', sender);
    });

    // Perform other off-chain tasks...

    // If you want to stop listening after a certain period, you can use a setTimeout
    // setTimeout(() => exampleContract.removeAllListeners('ItemAdded'), 60000); // Unsubscribe after 60 seconds
}

retrieveEvents();

Wrapping up

I want to thank you for reading through this article. I think “saving gas” is a valuable point in writing smart contracts, and all developers in the web3 field need to have a clear understanding of it. The patterns we talked about here, specifically, affect blockchain transactions, contract deployments & calls, and help save the user community on gas fees. Your comments and feedback will be valuable. So please do let me know about your opinions.

In the next article in the “Save Gas” series, I’d go through patterns that focus on keeping the storage costs lower.

Exit mobile version