State Variables¶
State variables store data permanently on the blockchain.
Declaring State Variables¶
contract Storage {
// Simple types
uint256 public count;
bool public active;
address public owner;
string public name;
// With initial values
uint256 public fee = 100;
bool public paused = false;
}
Visibility¶
Public¶
Creates automatic getter function:
uint256 public totalSupply;
// Automatically creates: function totalSupply() public view returns (uint256)
Private¶
Only accessible within the contract:
uint256 private _secretValue;
function getSecret() public view returns (uint256) {
return _secretValue; // Only way to access
}
Internal¶
Accessible in this contract and derived contracts:
contract Base {
uint256 internal _baseValue;
}
contract Derived is Base {
function getBaseValue() public view returns (uint256) {
return _baseValue; // Can access internal
}
}
Mappings¶
Key-value storage:
contract Balances {
// Simple mapping
mapping(address => uint256) public balances;
// Nested mapping
mapping(address => mapping(address => uint256)) public allowances;
// Mapping to struct
mapping(uint256 => User) public users;
function setBalance(address account, uint256 amount) public {
balances[account] = amount;
}
function setAllowance(address owner, address spender, uint256 amount) public {
allowances[owner][spender] = amount;
}
}
Solana Implementation
Mappings are implemented as Program Derived Addresses (PDAs) on Solana. Each key derives a unique account address.
Deleting Mapping Entries¶
Use delete to remove a mapping entry and reclaim rent:
contract UserRegistry {
mapping(address => uint64) public scores;
mapping(address => mapping(address => uint256)) public allowances;
function removeUser(address user) public {
delete scores[user]; // Closes PDA, refunds rent to signer
}
function revokeAllowance(address owner, address spender) public {
delete allowances[owner][spender]; // Works with nested mappings too
}
}
PDA Closing
When you delete a mapping entry, the underlying PDA account is closed and the rent (lamports) is returned to the transaction signer. This is the proper way to clean up mapping data on Solana.
Arrays¶
Dynamic Arrays¶
contract Arrays {
uint256[] public numbers;
address[] public members;
function addNumber(uint256 num) public {
numbers.push(num);
}
function removeLastNumber() public {
numbers.pop();
}
function getNumbersLength() public view returns (uint256) {
return numbers.length;
}
function getNumber(uint256 index) public view returns (uint256) {
require(index < numbers.length, "Index out of bounds");
return numbers[index];
}
}
Fixed Arrays¶
contract FixedArrays {
uint256[10] public fixedNumbers;
function setNumber(uint256 index, uint256 value) public {
require(index < 10, "Index out of bounds");
fixedNumbers[index] = value;
}
}
Structs¶
struct User {
address wallet;
uint256 balance;
uint256 createdAt;
bool active;
}
contract UserRegistry {
User[] public users;
mapping(address => User) public userByAddress;
mapping(address => uint256) public userIndex;
function createUser() public {
User memory newUser = User({
wallet: msg.sender,
balance: 0,
createdAt: block.timestamp,
active: true
});
userIndex[msg.sender] = users.length;
users.push(newUser);
userByAddress[msg.sender] = newUser;
}
function updateBalance(address wallet, uint256 amount) public {
userByAddress[wallet].balance = amount;
users[userIndex[wallet]].balance = amount;
}
}
Constants and Immutables¶
Constants¶
Compile-time constants:
contract Constants {
uint256 public constant MAX_SUPPLY = 1000000;
uint256 public constant FEE_PERCENT = 3;
address public constant TREASURY = 0x1234...;
function calculateFee(uint256 amount) public pure returns (uint256) {
return amount * FEE_PERCENT / 100;
}
}
Immutables¶
Set once in constructor:
contract Immutables {
address public immutable owner;
uint256 public immutable deployTime;
constructor() {
owner = msg.sender;
deployTime = block.timestamp;
}
}
State Initialization¶
In Declaration¶
uint256 public count = 0;
bool public active = true;
address public owner = msg.sender; // Set at deployment
In Constructor¶
contract Token {
string public name;
uint256 public totalSupply;
constructor(string memory _name, uint256 _supply) {
name = _name;
totalSupply = _supply;
}
}
Reading State¶
Direct Access¶
Mapping Access¶
Array Access¶
Modifying State¶
Simple Assignment¶
Increment/Decrement¶
function increment() public {
count += 1;
// Or: count = count + 1;
// Or: count++;
}
function decrement() public {
require(count > 0, "Cannot go below zero");
count -= 1;
}
Mapping Updates¶
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient");
balances[msg.sender] -= amount;
balances[to] += amount;
}
State Patterns¶
Ownership¶
contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previous, address indexed next);
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0), "Invalid address");
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}
Pausable¶
contract Pausable {
bool public paused;
modifier whenNotPaused() {
require(!paused, "Contract paused");
_;
}
modifier whenPaused() {
require(paused, "Contract not paused");
_;
}
function pause() public onlyOwner whenNotPaused {
paused = true;
}
function unpause() public onlyOwner whenPaused {
paused = false;
}
}
Counter¶
contract Counter {
uint256 private _counter;
function current() public view returns (uint256) {
return _counter;
}
function increment() public returns (uint256) {
_counter += 1;
return _counter;
}
function decrement() public returns (uint256) {
require(_counter > 0, "Counter underflow");
_counter -= 1;
return _counter;
}
}
Gas Considerations¶
Optimize Storage
- Pack smaller types together (e.g., multiple
uint128in one slot) - Use
uint256for single values (no packing overhead) - Use mappings instead of arrays when you don't need enumeration
- Cache storage reads in memory for multiple uses
// Less efficient - multiple storage reads
function inefficient() public view returns (uint256) {
return balance + balance + balance;
}
// More efficient - cache in memory
function efficient() public view returns (uint256) {
uint256 b = balance; // One storage read
return b + b + b;
}
Next Steps¶
- Control Flow - Conditionals and loops
- Events & Errors - Logging state changes
- Modifiers - State access control