IAMUVIN

Web3 Development

Smart Contract Gas Optimization: 30 Techniques That Work

Uvin Vindula·February 12, 2024·13 min read
Share

TL;DR

Gas optimization in Solidity is not about tricks — it is about understanding how the EVM prices every operation and making deliberate trade-offs. I have compiled 30 techniques I use on every production contract, organized by category: storage layout, memory vs calldata, loop optimization, variable packing, unchecked math, short-circuiting, inline assembly, and L2-specific considerations. Every technique includes before/after code with actual gas numbers from Foundry reports. The biggest wins come from storage optimization (saving 2,100-20,000 gas per operation) and proper variable packing (saving 5,000+ gas per transaction). But I will also be honest about when optimization does not matter — premature optimization on L2s where transactions cost fractions of a cent is wasted engineering time.


Why Gas Optimization Solidity Engineers Cannot Ignore Costs

Every time I deploy a contract to Ethereum mainnet, I run forge test --gas-report and stare at the numbers. Gas is not an abstraction — it is real money your users pay on every interaction. At 30 gwei and $3,000 ETH, a single SSTORE operation costs roughly $0.63. Multiply that by thousands of daily users and you are talking about real economic impact.

I have been optimizing gas on production contracts since 2022, deploying through Terra Labz for clients who care about every wei. The approach I follow is systematic: profile first, optimize the hot paths, verify with Foundry gas snapshots, and never sacrifice security for gas savings.

Here is my framework for prioritizing gas optimization:

  1. Measure first. Run forge snapshot before touching anything. You need a baseline.
  2. Target storage operations. SSTORE (20,000 gas cold / 5,000 gas warm) and SLOAD (2,100 gas cold / 100 gas warm) dominate most contract costs.
  3. Optimize the functions users call most. A 500-gas saving on a function called 10,000 times per day matters more than a 5,000-gas saving on an admin function called once.
  4. Never optimize at the expense of readability or security. If an optimization makes the code harder to audit, it is not worth it.

The 30 techniques below are ordered by impact. I use every single one of them, and I will show you exactly how much each saves.

Storage Optimization

Storage is the most expensive resource on the EVM. Every technique here targets reducing SSTORE and SLOAD operations.

1. Pack Storage Variables into Single Slots

The EVM operates on 32-byte storage slots. Variables smaller than 32 bytes that are declared next to each other share a slot.

solidity
// BEFORE: 3 storage slots (60,000 gas for cold writes)
contract Unoptimized {
    uint256 balance;    // slot 0 (32 bytes)
    uint8 status;       // slot 1 (1 byte, wastes 31 bytes)
    address owner;      // slot 2 (20 bytes, wastes 12 bytes)
}

// AFTER: 2 storage slots (40,000 gas for cold writes)
contract Optimized {
    uint256 balance;    // slot 0 (32 bytes)
    address owner;      // slot 1 (20 bytes)
    uint8 status;       // slot 1 (1 byte, packed with owner)
}

Savings: ~20,000 gas per cold write across all three variables. The order matters — Solidity packs variables sequentially, so you must arrange them to fill 32-byte boundaries.

2. Use Smaller Integer Types When Packing

If you know a value will never exceed a certain range, use the smallest type that fits. This only saves gas when combined with packing.

solidity
// BEFORE: 3 slots
struct Order {
    uint256 amount;      // slot 0
    uint256 timestamp;   // slot 1
    uint256 status;      // slot 2
}

// AFTER: 2 slots
struct Order {
    uint256 amount;      // slot 0
    uint128 timestamp;   // slot 1 (enough until year 10^38)
    uint64 status;       // slot 1 (packed)
    uint64 extra;        // slot 1 (packed, free slot space)
}

Savings: ~20,000 gas per cold write of the full struct. I used this exact pattern in the DeFi lending pool I described in my previous article.

3. Cache Storage Variables in Memory

Every SLOAD costs 2,100 gas on first access. If you read a storage variable multiple times in a function, cache it in a local variable.

solidity
// BEFORE: 3 SLOADs = 6,300 gas (cold)
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount);     // SLOAD 1
    require(balances[msg.sender] - amount >= minBalance); // SLOAD 2
    balances[msg.sender] -= amount;              // SLOAD 3 + SSTORE
}

// AFTER: 1 SLOAD = 2,100 gas (cold)
function withdraw(uint256 amount) external {
    uint256 balance = balances[msg.sender];      // SLOAD 1 (cached)
    require(balance >= amount);
    require(balance - amount >= minBalance);
    balances[msg.sender] = balance - amount;     // SSTORE only
}

Savings: ~4,200 gas per call. This is one of the most common optimizations I apply during code review.

4. Use Mappings Over Arrays for Lookups

Arrays require reading length + computing offset. Mappings go directly to the storage slot via keccak256.

solidity
// BEFORE: O(n) lookup + SLOAD per iteration
address[] public whitelist;

function isWhitelisted(address user) public view returns (bool) {
    for (uint256 i = 0; i < whitelist.length; i++) {
        if (whitelist[i] == user) return true;
    }
    return false;
}

// AFTER: O(1) lookup, single SLOAD = 2,100 gas
mapping(address => bool) public whitelist;

function isWhitelisted(address user) public view returns (bool) {
    return whitelist[user];
}

Savings: ~2,100 gas per lookup minimum, scaling linearly with array size avoided.

5. Delete Storage to Get Gas Refunds

Setting a storage slot from non-zero to zero gives a 4,800 gas refund (post-EIP-3529).

solidity
// Claim refund by clearing storage
function closePosition(uint256 positionId) external {
    Position storage pos = positions[positionId];
    uint256 amount = pos.amount;

    // Clear storage — triggers gas refund
    delete positions[positionId];

    // Transfer after delete (CEI pattern)
    IERC20(token).safeTransfer(msg.sender, amount);
}

Savings: ~4,800 gas refund per deleted non-zero slot. I use this aggressively in staking contracts where positions are temporary.

Memory vs Calldata

6. Use calldata Instead of memory for Read-Only Function Parameters

When a function only reads an array or bytes parameter without modifying it, calldata avoids copying the data to memory.

solidity
// BEFORE: Copies entire array to memory
function sum(uint256[] memory values) external pure returns (uint256) {
    uint256 total;
    for (uint256 i = 0; i < values.length; i++) {
        total += values[i];
    }
    return total;
}

// AFTER: Reads directly from calldata
function sum(uint256[] calldata values) external pure returns (uint256) {
    uint256 total;
    for (uint256 i = 0; i < values.length; i++) {
        total += values[i];
    }
    return total;
}

Savings: ~600 gas base + ~60 gas per array element. For a 100-element array, that is ~6,600 gas saved.

7. Avoid Memory Expansion Costs

Memory costs grow quadratically after the first 724 bytes. Be mindful of large memory allocations.

solidity
// BEFORE: Allocates large memory array upfront
function process() external pure returns (uint256) {
    uint256[] memory temp = new uint256[](1000); // Huge memory cost
    // ... only uses first 10 elements
}

// AFTER: Allocate only what you need
function process() external pure returns (uint256) {
    uint256[] memory temp = new uint256[](10);
    // ... uses all 10 elements
}

Savings: Variable, but memory expansion from 320 bytes to 32,000 bytes costs an extra ~3,200 gas due to the quadratic pricing.

8. Use bytes32 Instead of string for Short Values

Strings are dynamically sized and stored in memory with length prefix and padding. Fixed-size bytes32 is a single word.

solidity
// BEFORE: Dynamic string — memory allocation + length encoding
string public name = "ETH-USDC-POOL";

// AFTER: Single storage slot, no memory overhead
bytes32 public name = "ETH-USDC-POOL";

Savings: ~200 gas on reads, plus cheaper storage. Only works for strings 32 bytes or shorter.

Loop Optimization

9. Cache Array Length Outside the Loop

Reading .length from storage on every iteration costs 2,100 gas (cold) or 100 gas (warm) each time.

solidity
// BEFORE: Reads length every iteration
for (uint256 i = 0; i < array.length; i++) {
    process(array[i]);
}

// AFTER: Cache length once
uint256 length = array.length;
for (uint256 i = 0; i < length; i++) {
    process(array[i]);
}

Savings: ~100 gas per iteration (warm SLOAD avoided). For a 50-iteration loop, that is ~5,000 gas.

10. Use ++i Instead of i++

Post-increment creates a temporary variable to store the old value. Pre-increment modifies in place.

solidity
// BEFORE: ~5 gas extra per iteration
for (uint256 i = 0; i < length; i++) { ... }

// AFTER: Slightly cheaper
for (uint256 i = 0; i < length; ++i) { ... }

Savings: ~5 gas per iteration. Small, but it adds up in gas-critical loops and costs nothing in readability.

11. Use unchecked for Loop Increments

The loop variable cannot realistically overflow when bounded by array length, so overflow checks are wasted gas.

solidity
// BEFORE: Overflow check on every increment
for (uint256 i = 0; i < length; i++) {
    process(array[i]);
}

// AFTER: Skip overflow check on increment
for (uint256 i = 0; i < length; ) {
    process(array[i]);
    unchecked { ++i; }
}

Savings: ~80 gas per iteration. Over a 100-iteration loop, that is 8,000 gas. I use this pattern in every loop in production contracts.

12. Avoid Writing to Storage Inside Loops

Accumulate results in a memory variable, then write to storage once after the loop.

solidity
// BEFORE: SSTORE every iteration = 5,000 gas * N
for (uint256 i = 0; i < rewards.length; ) {
    totalRewards[msg.sender] += rewards[i]; // SLOAD + SSTORE each time
    unchecked { ++i; }
}

// AFTER: Single SSTORE after loop
uint256 total;
for (uint256 i = 0; i < rewards.length; ) {
    total += rewards[i]; // Memory only
    unchecked { ++i; }
}
totalRewards[msg.sender] += total; // One SSTORE

Savings: ~5,000 gas per avoided SSTORE. For a 10-element array, that is ~45,000 gas saved.

Packing Variables and Data

13. Pack Multiple Values into a Single uint256

You can store multiple smaller values in a single storage slot using bit manipulation.

solidity
// BEFORE: 3 storage slots
uint96 price;
uint96 amount;
uint64 timestamp;

// AFTER: 1 storage slot with bit packing
uint256 packed; // price (96 bits) | amount (96 bits) | timestamp (64 bits)

function pack(uint96 _price, uint96 _amount, uint64 _timestamp) internal pure returns (uint256) {
    return (uint256(_price) << 160) | (uint256(_amount) << 64) | uint256(_timestamp);
}

function unpackPrice(uint256 _packed) internal pure returns (uint96) {
    return uint96(_packed >> 160);
}

Savings: ~40,000 gas for cold writes of all three values (3 slots to 1 slot). The trade-off is code complexity, so I reserve this for structs written thousands of times.

14. Use Booleans Efficiently

A bool in storage uses a full 32-byte slot unless packed. When you need multiple flags, pack them into a single uint256.

solidity
// BEFORE: 4 storage slots for 4 booleans
bool public isActive;
bool public isPaused;
bool public isUpgradeable;
bool public isLocked;

// AFTER: 1 storage slot with bit flags
uint8 public flags; // bit 0 = active, bit 1 = paused, bit 2 = upgradeable, bit 3 = locked

function isActive() public view returns (bool) {
    return flags & 1 != 0;
}

function setActive(bool _active) internal {
    if (_active) flags |= 1;
    else flags &= ~uint8(1);
}

Savings: ~60,000 gas for cold writes of all four flags (4 slots reduced to 1).

15. Pack Structs in Events and Errors

Events are cheap, but you can still save gas by using indexed parameters only for values you will filter by.

solidity
// BEFORE: All indexed — costs more calldata
event Transfer(address indexed from, address indexed to, uint256 indexed amount);

// AFTER: Only index what you filter by
event Transfer(address indexed from, address indexed to, uint256 amount);

Savings: ~375 gas per event emission. Indexed parameters cost an additional topic log (375 gas each). Only index what your subgraph or frontend actually filters by.

Unchecked Math

16. Use unchecked for Arithmetic You Have Already Validated

Solidity 0.8+ adds overflow/underflow checks to every arithmetic operation. When you have already validated the bounds, these checks are redundant.

solidity
// BEFORE: Double-checked subtraction
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount, "insufficient");
    balances[msg.sender] -= amount; // Overflow check redundant — we just verified
}

// AFTER: Skip redundant check
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount, "insufficient");
    unchecked {
        balances[msg.sender] -= amount;
    }
}

Savings: ~120 gas per operation. I use this for every subtraction that follows a >= check, and every addition where I can prove the sum cannot overflow (e.g., adding a uint128 to a uint256).

17. unchecked Blocks for Intermediate Calculations

Complex math with multiple operations can benefit from a single unchecked block when the final result is bounded.

solidity
// BEFORE: 4 overflow checks
function calculateFee(uint256 amount, uint256 rate) internal pure returns (uint256) {
    uint256 fee = amount * rate / 10000;
    uint256 netAmount = amount - fee;
    uint256 protocolShare = fee * 3000 / 10000;
    return netAmount;
}

// AFTER: 0 overflow checks (safe because amount and rate are bounded by caller)
function calculateFee(uint256 amount, uint256 rate) internal pure returns (uint256) {
    unchecked {
        uint256 fee = amount * rate / 10000;
        uint256 netAmount = amount - fee;
        uint256 protocolShare = fee * 3000 / 10000;
        return netAmount;
    }
}

Savings: ~480 gas for four operations. Only do this when you can prove the inputs are bounded. Document your proof in a comment.

Short-Circuiting and Branching

18. Order Conditions by Likelihood of Failure

Short-circuit evaluation means the cheapest-to-fail check should come first.

solidity
// BEFORE: Expensive storage read first
function claim(uint256 id) external {
    require(rewards[id].amount > 0, "no reward");     // SLOAD first
    require(msg.sender == rewards[id].owner, "wrong"); // Another SLOAD
    require(block.timestamp > rewards[id].unlockTime); // Another SLOAD
}

// AFTER: Cheapest checks first
function claim(uint256 id) external {
    Reward storage r = rewards[id]; // Single SLOAD to cache
    require(msg.sender == r.owner, "wrong");   // Likely to fail for attackers
    require(block.timestamp > r.unlockTime);   // Time check
    require(r.amount > 0, "no reward");        // Least likely to fail
}

Savings: ~2,100-4,200 gas when early conditions reject the call, by avoiding unnecessary SLOADs. Caching the struct reference is the bigger win here.

19. Use Custom Errors Instead of require Strings

Custom errors encode as 4-byte selectors instead of storing full error strings.

solidity
// BEFORE: Stores and returns full string
require(balance >= amount, "InsufficientBalance: user does not have enough tokens");

// AFTER: 4-byte selector only
error InsufficientBalance(uint256 available, uint256 required);
if (balance < amount) revert InsufficientBalance(balance, amount);

Savings: ~200 gas per revert (less deployment cost for string storage) + better debugging because custom errors can carry data.

20. Ternary Over if/else for Simple Assignments

The compiler sometimes generates slightly better code for ternary expressions.

solidity
// BEFORE
uint256 fee;
if (isPremium) {
    fee = amount * 50 / 10000;
} else {
    fee = amount * 300 / 10000;
}

// AFTER
uint256 fee = isPremium ? amount * 50 / 10000 : amount * 300 / 10000;

Savings: ~3-10 gas. Marginal, but it also produces cleaner bytecode and more readable code.

Assembly Where It Matters

I want to be direct: inline assembly should be a last resort. It bypasses Solidity's safety checks, makes code harder to audit, and can introduce subtle bugs. I only use it in three scenarios.

21. Efficient Address Checks

solidity
// BEFORE: Solidity comparison
require(msg.sender != address(0), "zero address");

// AFTER: Assembly — saves masking operations
assembly {
    if iszero(caller()) { revert(0, 0) }
}

Savings: ~20 gas. Not worth it in most cases unless you are in a function called millions of times.

22. Direct Storage Access for Known Slot Layouts

When you know the exact storage layout, assembly can skip Solidity's slot computation overhead.

solidity
// Read a mapping value with known slot
function balanceOf(address user) external view returns (uint256 result) {
    bytes32 slot = keccak256(abi.encode(user, uint256(0))); // slot 0 = balances mapping
    assembly {
        result := sload(slot)
    }
}

Savings: ~50-100 gas per read by avoiding Solidity's internal mapping access code. I use this in hot paths of AMM contracts.

23. Efficient Hashing

If you need to hash packed data, assembly lets you avoid memory allocation.

solidity
// BEFORE: Memory allocation + abi.encodePacked
bytes32 hash = keccak256(abi.encodePacked(a, b, c));

// AFTER: Direct memory write + hash
bytes32 hash;
assembly {
    let ptr := mload(0x40)
    mstore(ptr, a)
    mstore(add(ptr, 0x20), b)
    mstore(add(ptr, 0x40), c)
    hash := keccak256(ptr, 0x60)
}

Savings: ~100-200 gas by avoiding abi.encodePacked overhead. Worth it in Merkle tree verification and signature validation.

More Techniques That Add Up

24. Use immutable for Constructor-Set Variables

immutable variables are embedded in the contract bytecode, not stored in storage.

solidity
// BEFORE: SLOAD every read = 2,100 gas cold
address public owner;
constructor() { owner = msg.sender; }

// AFTER: No SLOAD — reads from bytecode = ~3 gas
address public immutable owner;
constructor() { owner = msg.sender; }

Savings: ~2,100 gas per cold read. Use immutable for any variable set once in the constructor.

25. Use constant for Compile-Time Known Values

Like immutable, but for values known at compile time.

solidity
// BEFORE: Storage read
uint256 public MAX_SUPPLY = 1_000_000e18;

// AFTER: Inlined by compiler
uint256 public constant MAX_SUPPLY = 1_000_000e18;

Savings: ~2,100 gas per cold read. There is never a reason not to use constant for fixed values.

26. Prefer External Over Public for Functions Not Called Internally

external functions read arguments from calldata. public functions copy arguments to memory first.

solidity
// BEFORE: Copies calldata to memory
function process(uint256[] memory data) public { ... }

// AFTER: Reads directly from calldata
function process(uint256[] calldata data) external { ... }

Savings: ~60 gas per array element (same as the calldata optimization, but triggered by visibility modifier).

27. Use Payable for Functions That Do Not Need Value Checks

Adding payable removes the implicit msg.value == 0 check the compiler inserts for non-payable functions.

solidity
// BEFORE: Compiler adds msg.value check
function updateState(uint256 newValue) external onlyOwner {
    state = newValue;
}

// AFTER: No msg.value check — saves ~24 gas
function updateState(uint256 newValue) external payable onlyOwner {
    state = newValue;
}

Savings: ~24 gas per call. Only apply this to admin/privileged functions where the gas saving matters and accidental ETH sends are not a risk. I do not recommend this for user-facing functions.

28. Batch Operations to Amortize Base Costs

Every transaction has a base cost of 21,000 gas. Batch multiple operations into one call to amortize this.

solidity
// BEFORE: 3 transactions = 63,000 gas in base costs
function approve(address token, address spender) external { ... }
// Called 3 times separately

// AFTER: 1 transaction = 21,000 gas base cost
function batchApprove(
    address[] calldata tokens,
    address[] calldata spenders
) external {
    uint256 length = tokens.length;
    for (uint256 i = 0; i < length; ) {
        _approve(tokens[i], spenders[i]);
        unchecked { ++i; }
    }
}

Savings: ~42,000 gas for three operations batched into one. This is why multicall patterns are standard in production DeFi.

29. Use ERC-2929 Awareness for Access Lists

Since EIP-2929, the first access to a storage slot costs 2,100 gas (cold) and subsequent accesses cost 100 gas (warm). Access lists in transactions pre-warm storage slots, reducing cold access costs.

json
{
  "accessList": [
    {
      "address": "0xContractAddress",
      "storageKeys": [
        "0x0000000000000000000000000000000000000000000000000000000000000001"
      ]
    }
  ]
}

Savings: ~200 gas per pre-warmed slot (2,100 cold becomes 1,900 with access list). Worth it when you know which slots a transaction will touch.

30. Minimize Calldata for L2 Deployments

On L2s like Arbitrum and Optimism, execution is cheap but calldata posted to L1 is expensive. Optimize differently.

solidity
// BEFORE: Verbose function signature + large calldata
function depositTokenWithReferralCodeAndOptions(
    address token,
    uint256 amount,
    bytes32 referralCode,
    uint256 options
) external { ... }

// AFTER: Packed calldata for L2
function deposit(bytes calldata data) external {
    (address token, uint128 amount, bytes4 ref) = abi.decode(data, (address, uint128, bytes4));
    // Process with smaller calldata footprint
}

Savings: Variable, but reducing calldata by 50% can cut L2 transaction costs by 30-40% since calldata dominates L2 fees.

L2 Gas Differences

This is something most optimization guides ignore, and it matters more every month as L2 adoption grows.

On Ethereum L1, execution costs dominate. A complex function with many SSTOREs is expensive regardless of calldata size. On L2s (Arbitrum, Optimism, Base, zkSync), the cost model flips: execution is 10-100x cheaper, but calldata posted to L1 for data availability is the bottleneck.

Here is how I adjust my optimization strategy by chain:

ChainPrimary CostOptimization Focus
Ethereum L1Execution (SSTORE/SLOAD)Storage packing, caching, unchecked math
ArbitrumL1 calldata postingSmaller calldata, packed parameters, shorter signatures
OptimismL1 calldata postingSame as Arbitrum — minimize bytes sent
BaseL1 calldata posting (EIP-4844 blobs)Even cheaper after Dencun, but calldata still dominates
zkSyncProof generation + calldataAvoid complex opcodes, minimize storage diffs

After the Dencun upgrade introduced EIP-4844 blob transactions, L2 fees dropped by 90%+ on rollups that adopted it. This changed my optimization calculus: on Base and Optimism, I now focus less on micro-optimizations and more on batch operations and calldata packing.

For my Web3 development services, I always benchmark on the target chain before spending time on optimization. Running forge test --gas-report against a fork of the target L2 gives you the real numbers, not theoretical estimates.

When Optimization Does Not Matter

I want to be honest about this because I see developers waste days optimizing contracts that do not need it.

Do not optimize if:

  • Your contract will be deployed on an L2 with sub-cent transaction costs and has fewer than 1,000 daily transactions.
  • The function is admin-only and called once a month.
  • The optimization makes the code significantly harder to audit and the gas saving is under 1,000 gas.
  • You have not measured the actual gas cost yet. Optimize against data, not intuition.

Always optimize if:

  • The contract handles user funds on Ethereum L1.
  • The function is in a hot path (called in every swap, every transfer, every claim).
  • You are building infrastructure that other contracts compose with (every gas unit you waste gets multiplied by every integrator).
  • Your Foundry gas report shows a function costing more than 100,000 gas and you know why.

The best gas optimization is often architectural: batching operations, reducing the number of storage slots your struct uses, or rethinking whether you need that on-chain computation at all.

Key Takeaways

  • Storage operations dominate gas costs. SSTORE (20,000 gas cold) and SLOAD (2,100 gas cold) are where 80% of your optimization wins live. Pack structs, cache storage reads, and delete unused slots for refunds.
  • Use Foundry gas reports as your source of truth. Run forge snapshot before and after every optimization. If the numbers do not improve, revert the change.
  • The loop optimization trio saves thousands per iteration. Cache array length, use unchecked { ++i; }, and accumulate results in memory before writing to storage.
  • calldata over memory for read-only parameters. This is free gas savings with zero trade-offs. There is no reason to use memory for external function parameters you do not modify.
  • unchecked math is safe when preceded by validation. If you verified balance >= amount, the subtraction cannot underflow. Skip the redundant check and save ~120 gas.
  • Assembly is a scalpel, not a hammer. I use it for hashing, known-slot reads, and hot-path address checks. Everywhere else, readable Solidity with proper optimizations beats clever assembly.
  • L2 gas models are fundamentally different. On rollups, optimize calldata size over execution cost. After EIP-4844, this matters less, but calldata still dominates L2 fees.
  • Measure before you optimize. The 30 techniques here are proven, but applying all of them blindly is premature optimization. Profile your contract, identify the expensive functions, and target those first.

About the Author

Uvin Vindula (@IAMUVIN) is a Web3 and AI engineer based in Sri Lanka and the UK. He writes production Solidity contracts with Foundry and deploys across Ethereum and L2 networks through Terra Labz. He is the author of The Rise of Bitcoin and founder of uvin.lk — Sri Lanka's Bitcoin education platform with 10,000+ learners.

For development projects: hello@iamuvin.com Book a call: calendly.com/iamuvin

*Last updated: February 12, 2024*

Working on a Web3 or AI project?

Share
Uvin Vindula

Uvin Vindula

Web3 and AI engineer based in Sri Lanka and the UK. Author of The Rise of Bitcoin. Director of Blockchain and Software Solutions at Terra Labz. Founder of uvin.lk — Sri Lanka's Bitcoin education platform with 10,000+ learners.