2

In this boilerplate by Hardhat, it stores user account balances in a state variable. In my very limited understanding, storing data on-chain will be expensive. Does it mean the more users using the smart contract, the more it'll cost to run the contract? Is it okay to keep a map that might grow indefinitely in a state variable? Or is there a better alternative? Perhaps storing it in an IPFS?

Below is a copy of the code:

//SPDX-License-Identifier: UNLICENSED

// Solidity files have to start with this pragma.
// It will be used by the Solidity compiler to validate its version.
pragma solidity ^0.8.9;

// We import this library to be able to use console.log
import "hardhat/console.sol";


// This is the main building block for smart contracts.
contract Token {
    // Some string type variables to identify the token.
    string public name = "My Hardhat Token";
    string public symbol = "MHT";

    // The fixed amount of tokens stored in an unsigned integer type variable.
    uint256 public totalSupply = 1000000;

    // An address type variable is used to store ethereum accounts.
    address public owner;

    // A mapping is a key/value map. Here we store each account balance.
    mapping(address => uint256) balances;

    // The Transfer event helps off-chain aplications understand
    // what happens within your contract.
    event Transfer(address indexed _from, address indexed _to, uint256 _value);

    /**
     * Contract initialization.
     */
    constructor() {
        // The totalSupply is assigned to the transaction sender, which is the
        // account that is deploying the contract.
        balances[msg.sender] = totalSupply;
        owner = msg.sender;
    }

    /**
     * A function to transfer tokens.
     *
     * The `external` modifier makes a function *only* callable from outside
     * the contract.
     */
    function transfer(address to, uint256 amount) external {
        // Check if the transaction sender has enough tokens.
        // If `require`'s first argument evaluates to `false` then the
        // transaction will revert.
        require(balances[msg.sender] >= amount, "Not enough tokens");

        // We can print messages and values using console.log, a feature of
        // Hardhat Network:
        console.log(
            "Transferring from %s to %s %s tokens",
            msg.sender,
            to,
            amount
        );

        // Transfer the amount.
        balances[msg.sender] -= amount;
        balances[to] += amount;

        // Notify off-chain applications of the transfer.
        emit Transfer(msg.sender, to, amount);
    }

    /**
     * Read only function to retrieve the token balance of a given account.
     *
     * The `view` modifier indicates that it doesn't modify the contract's
     * state, which allows us to call it without executing a transaction.
     */
    function balanceOf(address account) external view returns (uint256) {
        return balances[account];
    }
}

1 Answer 1

2

It really depends on what you are doing with the data.

If you have a storage array that you add elements to it and then you loop over that array in a function, yes, your contract could start spending more and more gas until it's unusable. Unless you also remove elements from it. Looping through a storage array to find one element is expensive because the time complexity for that operation is linear, O(n). If you have 1000 elements, the worst-case scenario is that you need to go through 1000 iterations to find your item.

But for mappings, you don't loop through its elements. You access them one by one, and that happens in constant time, O(1) (no matter how many elements you have in the mapping, you will always do the exact same operations to find one element in it, meaning it will spend the same gas). So a mapping could grow and grow and still be cheap to use. Besides, if you have logic to remove elements from the mapping, or the values in the mapping are set to the default value, then they don't hold any space and it will be lighter.

For example:

If you have this mapping:

mapping(address => uint256) public balances;

And you add some balance to some addresses. And then you reduce the balance of some of those addresses to 0 (which is the default value for a uint256), then the contract 'resets' the space that balances mapping was using for that address.

Storing a lot of data in a smart contract in theory is expensive because you are spending gas to store data in the storage every time you add new data to it. Having the data sitting there is not that expensive unless you need to read it many times. What is more expensive is looping through the data, as I explained above when using arrays, we should take our time to design a good solution that will not grow massively and deem the contract unusable.

An experienced developer could use a combination of mapping and arrays to create a solution that is efficient. Also could have logic to take care of data that is no longer needed and clear the storage. The EVM rewards you with some gas refund when you clear data from storage and also from memory when the contract is running. https://github.com/ethereum/EIPs/issues/1048

Your example contract looks alright and doesn't seem like it will be more costly to run in the future. You are using mappings and you are not looping through anything.

An article that might be useful to understand how the storage data is stored and accessed depending on the type:

https://coinsbench.com/solidity-layout-and-access-of-storage-variables-simply-explained-1ce964d7c738

EVM opcodes and how much gas they consume:

https://github.com/crytic/evm-opcodes

Especially SLOAD which reads a word (32 bytes) from storage, costs 800 gas.

And SSTORE which writes data to the storage, starting at 20000 gas.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.