Solidity 101: A Starter Guide
Table of Contents
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
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.
Usage:
Local variables within functions can be declared with the
memory
keyword.Function arguments can also be explicitly set to
memory
.
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:
Persistent: Unlike memory variables, storage variables persist between function calls and transactions. The data is stored directly on the Ethereum blockchain.
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:
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.
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
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 frommemory
orstorage
.
Usage:
- Used in
external
function arguments to signify that the data should not be modified and will be kept in the transaction's calldata.
- Used in
Example:
solidityCopy code
pragma solidity ^0.8.0;
contract CalldataExample {
function calldataFunction(uint256[] calldata data) external pure returns (uint256) {
return data.length;
}
}
Comparative Analysis
Aspect | Storage | Memory | Calldata |
Lifetime | Persistent | Temporary | Function call |
Cost | High (Gas) | Lower (Gas) | Lowest (Gas) |
Mutability | Mutable | Mutable | Immutable |
Location | Blockchain State | Function Scope | Transaction |
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
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.
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;
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.
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
- 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
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
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
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 🌐