Solidity 101: A Starter Guide

Pratik TiwariPratik Tiwari
12 min read

Table of Contents

  1. Datatypes

    1. Elementary Types

    2. Complex Types

    3. Special Types

    4. Storage vs Memory Datatypes vs Calldata

  2. Contracts

    1. Definition

    2. Events

    3. Scopes

    4. Inheritance

    5. Cross contract calls

    6. Modifiers

  3. Gas Optimzation Techniques

    1. Use Appropriate Data Types

    2. Use events for output data

    3. Short-Circuiting in Conditions

    4. Loops and Large Data Structures

    5. Use Libraries and Delegatecall

  4. Payabale

  5. Withdraw

  6. Resources

Datatypes

Elementary Types

Boolean

Utilised for logical operations. Two possible values: true and false.

bool isTrue = true;

Integer

Supports both signed (int) and unsigned (uint) integers.

int256 a = -10;
uint256 b = 10;

Address

Used to store ethereum addresses. These addresses are link the bank account number for your bank account.

address public owner;

Bytes

Fixed-size byte arrays ranging from byte1 to byte2.

bytes32 hash = "abc";

Complex Types

Arrays

can be either fixed-size or dynamic.

uint256[] public numbers;

Structs

Structures (also called structs) are a way to group several related variables into one place.

struct Person {
    string name;
    uint age;
}

Enums

Enumeration types for creating custom types with a finite set of values.

enum State { Created, Locked, Inactive }

Special Types

Mappings

Key-value storage Structures

mappings(addresses => uint) public balances;

Function Types

These are first-class citizens in solidity. allowing for higher-order functions.

function (uint256) external returns (bool) myFunction;

Storage vs Memory Datatypes

Understanding the distinction between storage and memory data types is crucial for efficient and secure smart contract development. These two data locations serve different purposes and have different cost implications in terms of gas usage.

Characteristics

  • Persistent: Data stored in storage persists between function calls and transactions. Changes to storage are very costly in terms of gas.

  • Blockchain State: Storage variables are part of the contract's state, and thus, they are written to the Ethereum blockchain.

Usage

  • State variables are by default storage variables.

  • Explicitly, the storage keyword can be used in function arguments, although this is rarely done due to high gas costs.

Memory Data Types

  1. Characteristics:

    • Temporary: Memory variables only exist during the execution of a function. Once the function execution is complete, the data stored in memory is erased.

    • Lower Gas Costs: Reading from and writing to memory is generally less costly than storage operations.

  2. Usage:

    • Local variables within functions can be declared with the memory keyword.

    • Function arguments can also be explicitly set to memory.

  3. Example:

    
     contract MemoryExample {
         function memoryFunction() public pure returns (uint256) {
             uint256[] memory memoryArray = new uint256[](3);
             memoryArray[0] = 1;
             memoryArray[1] = 2;
             memoryArray[2] = 3;
             return memoryArray[1];  // Returns 2
         }
     }
    

Storage Data Types

Characteristics:

  1. Persistent: Unlike memory variables, storage variables persist between function calls and transactions. The data is stored directly on the Ethereum blockchain.

  2. Higher Gas Costs: Any operation that alters the state of storage variables incurs a higher gas fee compared to operations on memory variables. This is due to the permanent nature of storage and the broader impact it has on the Ethereum network.

Usage:

  1. State Variables: By default, variables declared at the contract level are storage variables. They represent the contract's state and exist for the life of the contract.

  2. Explicit Keyword: Although rarely used, the storage keyword can explicitly mark certain local variables, generally in the context of referencing or aliasing state variables.

Example:


contract StorageExample {
    // State variable stored on the blockchain
    uint256 public storageVariable;

    // Constructor to initialize the state variable
    constructor(uint256 _initialValue) {
        storageVariable = _initialValue;
    }

    // Function to update the state variable
    function setStorageVariable(uint256 _value) public {
        storageVariable = _value;
    }

    // Function to get the state variable
    function getStorageVariable() public view returns (uint256) {
        return storageVariable;
    }
}

Calldata

  1. Characteristics:

    • Immutable: Calldata is a read-only data location.

    • External Functions: Only applicable for external function arguments.

    • Lowest Cost: Reading from calldata is cheaper than from memory or storage.

  2. Usage:

    • Used in external function arguments to signify that the data should not be modified and will be kept in the transaction's calldata.
  3. Example:

    solidityCopy code

    pragma solidity ^0.8.0;

    contract CalldataExample {
        function calldataFunction(uint256[] calldata data) external pure returns (uint256) {
            return data.length;
        }
    }

Comparative Analysis

AspectStorageMemoryCalldata
LifetimePersistentTemporaryFunction call
CostHigh (Gas)Lower (Gas)Lowest (Gas)
MutabilityMutableMutableImmutable
LocationBlockchain StateFunction ScopeTransaction

Contracts

Definition

A contract in Solidity is a collection of code and data that resides at a specific address on the Ethereum blockchain. It serves as the fundamental building block for creating decentralized applications.

contract SimpleContract {
    // your logic here 
}

it can have methods, state variables, modifiers, events etc.

contract ComplexContract {
    uint256 public stateVariable;

    modifier onlyOwner() {
        // Modifier code
        _;
    }

    event LogData(uint256 data);

    function doSomething() public onlyOwner {
        emit LogData(stateVariable);
    }
}

Events

In Solidity, events are a crucial feature that facilitates communication between smart contracts and their external consumers, such as decentralized applications (dApps). Events provide a logging mechanism that allows for the storage of arguments in the transaction logs—a component of the Ethereum blockchain. This feature is particularly valuable for external clients that may need to "listen" for specific occurrences within a smart contract.

event LogData(uint indexed id, string message);

Emiting Events

emit LogData(1, "This is a message");

Example

pragma solidity ^0.8.0;

contract EventExample {
    event LogChange(string oldValue, string newValue);

    string public data;

    function setData(string memory _data) public {
        emit LogChange(data, _data);
        data = _data;
    }
}

Scopes

Global Scopes

At the global scope, you can declare contracts, import other contracts or libraries, and define custom data types like structs and enums.

import "./AnotherContract.sol";

contract GlobalScopeExample {
    // Code and data go here
}

Contracts Scopes

Within a contract, you can define state variables, functions, modifiers, and events. These elements are accessible based on their visibility modifiers (public, internal, private, external).

Public

The public modifier allows the widest level of accessibility. Functions marked as public can be called both internally within the contract and externally by other contracts or external actors. For state variables, Solidity automatically generates a getter function when they are marked as public.

pragma solidity ^0.8.0;

contract PublicExample {
    uint256 public stateVariable;

    function publicFunction() public returns (uint256) {
        return stateVariable;
    }
}

Internal

The internal modifier restricts access to the current contract and derived contracts (i.e., contracts that inherit from it). Functions and state variables marked as internal cannot be accessed by external contracts or external actors.

pragma solidity ^0.8.0;

contract InternalExample {
    uint256 internal stateVariable;

    function internalFunction() internal returns (uint256) {
        return stateVariable;
    }
}

Private

The private modifier imposes the most restrictive level of access. Functions and state variables marked as private can only be accessed within the contract where they are defined. Even derived contracts cannot access private members of their base contracts.

pragma solidity ^0.8.0;

contract PrivateExample {
    uint256 private stateVariable;

    function privateFunction() private returns (uint256) {
        return stateVariable;
    }
}

External

The external modifier is similar to public, but it restricts the function to only be callable from outside the contract. Functions marked as external cannot be called internally within the contract, except using this.functionName().

pragma solidity ^0.8.0;

contract ExternalExample {
    function externalFunction() external pure returns (string memory) {
        return "This function can only be called externally.";
    }
}

Inheritance

In Solidity, inheritance is a powerful feature that allows for the creation of new contracts based on existing ones. This concept enables code reusability, a hierarchical structure of contracts, and the development of complex decentralized applications with modular components. Understanding inheritance is essential for achieving a high level of technical competence in Solidity.

pragma solidity ^0.8.0;

contract ParentContract {
    function parentFunction() public pure returns (string memory) {
        return "This is from the parent contract";
    }
}

contract ChildContract is ParentContract {
    function childFunction() public pure returns (string memory) {
        return parentFunction();
    }
}

Cross contracts calls

Interface calls

Interfaces provide a way to define the functions of the external contract, offering a more modular approach.

pragma solidity ^0.8.0;

interface IContractA {
    function foo() external pure returns (string memory);
}

contract ContractB {
    IContractA contractA;

    // why use external here? 
    // Since constructors are only executed once, any logic or state initialization placed in the constructor does not incur repeated gas costs. This is more efficient than initializing or setting these variables in separate transactions after deployment.

    constructor(address _contractA) external {
        contractA = IContractA(_contractA);
    }

    function callFoo() public view returns (string memory) {
        return contractA.foo();
    }
}

Modifiers

Think it as a middleware in web2 world.

In Solidity, modifiers are a powerful feature that allows for the modification of function behavior in a reusable and clean manner. They are often used to simplify code, enhance readability, and manage permissions or conditions that must be met before a function can execute. Understanding the role and utility of modifiers is pivotal for anyone aiming to achieve a high level of technical competence in smart contract development.

modifier onlyOwner() {
    require(msg.sender == owner, "You are not the owner");
    _;
}

Usage in Functions

Modifiers are appended to function signatures and modify the function's behavior. Multiple modifiers can be used, and they are executed in the order in which they appear.

function doSomething() public onlyOwner {
    // Function logic here
}

Example

Here's a simple example demonstrating the use of a modifier for access control:

pragma solidity ^0.8.0;

contract ModifierExample {
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner, "You are not the owner");
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function setOwner(address _newOwner) public onlyOwner {
        owner = _newOwner;
    }
}

In this example, the onlyOwner modifier ensures that only the contract's owner can change the owner address. The setOwner function uses this modifier to restrict access.

Gas Optimzation Techniques

Use Appropriate Data Types

  1. Use uint256 for Arithmetic: The EVM is optimised for 256-bit arithmetic, making uint256 often more gas-efficient than smaller integer types like uint8 or uint16.

  2. Use bytes1 to bytes32 for Small Data: Fixed-size byte arrays are more gas-efficient than their dynamic bytes counterpart.

// Prefer uint256 for arithmetic
uint256 public counter;

// Use bytes32 for fixed-size byte arrays
bytes32 public fixedHash;
  1. Using calldata as an arguments when you know the arguments arent changing as it is stored in different read only memory which is very gas efficient. If the change doesnt need to be propograted use memory as it cost efficent in comparison to storage which changes are propograted to the whole contract.

  2. Optimizing on space complexity over time complexity. Suppose you have a function which iterates over a array and returns a sum. You can also store this sum whenever a value is addded and create a variable in the contract for the same. But suppose the function is just a view function you dont have much incentive to save on time complexity because any which ways the gas is not used for a view function computing and gas is used to store the variable. Hence compromising on the time compleixity here makes more sense to save on the gas.

Use Events for Output Data

  1. Log Events Instead of Return: For data that doesn't need to be immediately used in transactions, consider using events. They are cheaper than storing data.

solidityCopy code

event DataEvent(uint256 data);

function doSomething() public {
    // ... logic
    emit DataEvent(42);
}

Short-Circuiting in Conditionals

Order Conditions Wisely: In if and require statements, place the most likely-to-fail conditions first. This takes advantage of short-circuiting to save gas.

require(isValid && (msg.sender == owner), "Condition failed");

Loops and Large data Structures

Avoid Infinite Loops: Always ensure that loops have a termination condition. Use Pagination for Large Data Sets: When dealing with large data structures, consider implementing pagination to read/write data in chunks.

// Pagination example
function getElements(uint256 start, uint256 end) public view returns (uint256[] memory) {
    uint256[] memory elements = new uint256[](end - start);
    for (uint256 i = start; i < end; i++) {
        elements[i - start] = someLargeArray[i];
    }
    return elements;
}

Use Libraries and Delegatecall

  1. se Libraries for Reusable Logic: Code that is reused across multiple contracts can be placed in a library, reducing the deployment and runtime gas costs. Use delegatecall Carefully: This low-level function allows for more gas-efficient logic

  2. execution but should be used cautiously due to its complexity and potential securit risks.

// Using a library function
using SafeMath for uint256;

uint256 public value;

function increment(uint256 _value) public {
    value = value.add(_value);
}

Payable

In Solidity, the payable keyword is used to enable a function or a contract to receive Ether. This feature is crucial for the development of financial applications and other smart contracts that involve monetary transactions. Understanding the payable modifier is essential for anyone aiming to achieve a high level of technical competence in smart contract development.

function deposit() public payable {
    // Function logic here
}

Example

pragma solidity ^0.8.0;

contract PayableExample {
    uint256 public balance;

    function deposit() public payable {
        balance += msg.value;
    }

    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

Withdraw

In Solidity, withdraw functions are a critical component of smart contracts that handle Ether transactions. These functions facilitate the secure transfer of Ether from a contract to an external address. Understanding the design patterns and security considerations for implementing withdraw functions is essential for anyone aiming to achieve a high level of technical competence in smart contract development.

Think of it as a function to get out the eths by the contract maker to his or someone elses address

Security: Withdraw functions must be carefully designed to prevent vulnerabilities such as reentrancy attacks.

Gas Efficiency: Efficiently coded withdraw functions can minimize gas costs, making the contract more economical to use.

Access Control: Usually, withdraw functions incorporate access control mechanisms to ensure that only authorized addresses can withdraw funds.

function withdraw(uint256 amount) public {
    require(amount <= balances[msg.sender], "Insufficient balance");
    payable(msg.sender).transfer(amount);
    balances[msg.sender] -= amount;
}

Example

pragma solidity ^0.8.0;

contract WithdrawExample {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // Securely perform the transfer
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Withdrawal failed");

        // Update state after the transfer
        balances[msg.sender] -= amount;
    }
}

In this example, the withdraw function first checks whether the caller has a sufficient balance. It then uses a low-level .call method to perform the transfer, which is currently recommended over .transfer or .send to mitigate certain types of reentrancy attacks. Finally, it updates the balance mapping to reflect the withdrawal.

Resources

0
Subscribe to my newsletter

Read articles from Pratik Tiwari directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Pratik Tiwari
Pratik Tiwari

fullstack dev at a bay area startup ✶ Incosistent open source contributor ✶ Go ✶ JS ✶ web3 🌐