State Variables - Solidity Part 2.1

In Solidity, the concept of state variables is foundational for developing robust and efficient smart contracts. State variables are variables whose values are permanently stored on the blockchain. Let’s dive into the intricacies of state variables, their usage, and best practices to maximize their potential.

Source Code: https://github.com/scaihai/enkwadore-blog-blockchain-demos/tree/main/solidity/contracts/2.1

What Are State Variables?

State variables are declared at the contract level and are stored on the blockchain. This persistent storage ensures that the values of these variables are maintained across different function calls and transactions. They are crucial for maintaining the state of your smart contract.

Declaring State Variables

State variables are declared similarly to local variables, but they are defined outside of any function. Here’s an example:

MyContract.sol

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

contract MyContract {
    // State variables
    uint public myNumber;
    string public myString;
    address public myAddress;
    
    // Constructor to initialize state variables
    constructor(uint _number, string memory _string, address _address) {
        myNumber = _number;
        myString = _string;
        myAddress = _address;
    }
}

In this example, myNumber, myString, and myAddress are state variables. They are stored on the blockchain, and their values persist between function calls.

Visibility Modifiers

State variables can have different visibility levels:

  • Public: The variable is accessible both within the contract and externally. Solidity automatically creates a getter function for public state variables.
  • Internal: The variable is accessible only within the contract and contracts deriving from it. This is the default visibility level.
  • Private: The variable is accessible only within the contract it is defined in. Derived contracts cannot access it.
  • External: This visibility is not applicable to state variables; it’s used for functions.

Here’s an example illustrating the different visibility levels:

VisibilityExample.sol

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

contract VisibilityExample {
    uint public publicVariable = 1; // accessible everywhere
    uint internal internalVariable = 2; // accessible within the contract and derived contracts
    uint private privateVariable = 3; // accessible only within this contract
    
    function getPrivateVariable() public view returns (uint) {
        return privateVariable;
    }
}

Default Values

If you do not initialize a state variable, Solidity assigns a default value based on its type. For instance:

  • uint defaults to 0.
  • bool defaults to false.
  • address defaults to 0x0000000000000000000000000000000000000000.

Gas Costs

State variables are stored on the blockchain, which incurs gas costs. Reading state variables is cheaper than writing to them. Thus, it’s good practice to minimize the number of state variable writes to optimize gas usage.

Best Practices

  1. Minimize Storage Operations: Since writing to state variables is costly, try to minimize the number of write operations.
  2. Use Memory and Calldata: For temporary data within functions, use memory or calldata instead of storage to save gas.
  3. Encapsulation: Use internal or private visibility to encapsulate state variables and provide controlled access through functions.
  4. Naming Conventions: Use clear and descriptive names for state variables to enhance code readability and maintainability.

Example: Managing State Variables

The following is a practical example of managing state variables in a contract. The contract effectively holds the funds (Ether) of the users that deposit into it.

Bank.sol

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

contract Bank {
    mapping(address => uint) private balances;
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    function deposit() public payable {
        require(msg.value > 0, "Deposit value must be greater than zero");
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint _amount) public {
        require(balances[msg.sender] >= _amount, "Insufficient balance");
        balances[msg.sender] -= _amount;
        payable(msg.sender).transfer(_amount);
    }

    function getBalance() public view returns (uint) {
        return balances[msg.sender];
    }
}

Now let’s discuss this Bank contract in detail:

State Variables

  • balances: This is a state variable of type mapping, which maps an address to a uint. It’s used to store the balance of each user. The private keyword means that this variable can only be accessed within the contract.
  • owner: This state variable stores the address of the contract owner and is declared public, so it automatically gets a getter function.

Constructor

The constructor is a special function that is executed once when the contract is deployed. It sets the owner to the address of the account that deploys the contract (msg.sender).

Deposit Function

  • function deposit() public payable: This function allows users to deposit Ether (or the native currency of an EVM compatible blockchain) into the contract. The payable keyword indicates that the function can receive Ether.
  • msg.value is a special global variable in Solidity that represents the amount of Wei (the smallest unit of Ether) sent with a transaction.
  • When a function marked as payable is called, msg.value contains the amount of Ether sent by the caller.
  • require(msg.value > 0, "Deposit value must be greater than zero"): This line ensures that the deposit amount is greater than zero. If not, the transaction reverts with an error message.
  • balances[msg.sender] += msg.value: The balance of the sender (msg.sender) is increased by the amount of Ether sent (msg.value).

Withdraw Function

  • function withdraw(uint _amount) public: This function allows users to withdraw a specified amount of Ether from the contract.
  • require(balances[msg.sender] >= _amount, "Insufficient balance"): This line ensures that the user has enough balance to withdraw the specified amount. If not, the transaction reverts with an error message.
  • balances[msg.sender] -= _amount: The balance of the sender (msg.sender) is decreased by the withdrawal amount.
  • payable(msg.sender).transfer(_amount): The specified amount of Ether is transferred to the sender’s address.

Get Balance Function

  • function getBalance() public view returns (uint): This is a read-only function (indicated by view) that returns the balance of the caller. This means that it does not modify the value of the state variables.
  • return balances[msg.sender]: It returns the balance of the caller (msg.sender).

Conclusion

Understanding and effectively using state variables is essential for building efficient and secure smart contracts in Solidity. By following best practices and optimizing gas usage, you can enhance the performance and reliability of your contracts.

Share:
spacer