bedda.tech logobedda.tech
← Back to blog

EVM Gas Optimization: Crowdia Smart Contract Lessons

Matthew J. Whitney
6 min read
blockchainsmart contractsweb3cryptocurrencydefi

Here's the deal: EVM gas optimization isn't just about writing clever Solidity code — it's about building a system that doesn't bankrupt your users or crash your UI when transactions fail. I learned this the hard way building Crowdia, our crowdfunding platform with smart contract escrow.

Most gas optimization guides focus on theoretical savings. I'm going to show you what actually matters in production, based on real user behavior and the painful lessons we learned when our early users hit $50+ transaction fees during network congestion.

The Real Cost of Bad Smart Contract Design

When we first deployed Crowdia's escrow contracts, I made every rookie mistake in the book. Our initial contract had nested loops, redundant storage writes, and zero consideration for batch operations. The result? Campaign creators were paying 300,000+ gas just to initialize a basic crowdfunding campaign.

Here's what our original campaign creation function looked like:

function createCampaign(
    string memory title,
    string memory description,
    uint256 goal,
    uint256 deadline,
    address[] memory rewards
) public {
    campaigns[campaignCounter] = Campaign({
        creator: msg.sender,
        title: title,
        description: description,
        goal: goal,
        deadline: deadline,
        raised: 0,
        active: true
    });
    
    // This was the killer - individual storage writes
    for (uint i = 0; i < rewards.length; i++) {
        campaignRewards[campaignCounter][i] = rewards[i];
        emit RewardAdded(campaignCounter, i, rewards[i]);
    }
    
    campaignCounter++;
    emit CampaignCreated(campaignCounter - 1, msg.sender);
}

The problem wasn't just the loop — it was that we were doing individual storage operations for each reward tier. On Ethereum mainnet during peak times, this was costing users real money.

Blockchain Optimization: What Actually Moves the Needle

After analyzing gas reports and user complaints, I focused on three areas that gave us the biggest bang for our optimization buck:

1. Packed Structs and Storage Layout

The Solidity documentation is clear about storage packing, but most developers ignore it. We restructured our Campaign struct to pack variables efficiently:

struct Campaign {
    address creator;        // 20 bytes
    uint96 goal;           // 12 bytes - fits in same slot
    uint96 raised;         // 12 bytes
    uint32 deadline;       // 4 bytes - fits in same slot  
    bool active;           // 1 byte
    uint8 rewardCount;     // 1 byte
    // strings stored separately to avoid bloating the struct
}

This single change reduced our campaign creation gas cost by 40,000 gas — about $12 at peak network congestion.

2. Batch Operations Over Individual Calls

The biggest win came from rethinking how users interact with our contracts. Instead of individual contribution transactions, we implemented a batch contribution system:

function batchContribute(
    uint256[] calldata campaignIds,
    uint256[] calldata amounts
) external payable {
    require(campaignIds.length == amounts.length, "Array length mismatch");
    
    uint256 totalRequired = 0;
    for (uint i = 0; i < amounts.length; i++) {
        totalRequired += amounts[i];
    }
    require(msg.value == totalRequired, "Incorrect ETH amount");
    
    for (uint i = 0; i < campaignIds.length; i++) {
        _contribute(campaignIds[i], amounts[i]);
    }
}

Users could now back multiple campaigns in a single transaction, splitting the base transaction cost across multiple actions.

Smart Contracts: Handling Failed Transactions Gracefully

Here's what most guides miss: your UI needs to handle gas estimation failures and provide meaningful fallbacks. When network congestion hits and gas prices spike, your users need options.

We implemented a three-tier gas strategy:

  1. Conservative estimation (success rate: ~95%)
  2. Standard estimation (success rate: ~85%, 20% cheaper)
  3. Aggressive estimation (success rate: ~70%, 40% cheaper)

Our frontend estimates gas for all three scenarios and presents users with clear tradeoffs:

const estimateContributionGas = async (campaignId: number, amount: string) => {
  try {
    const baseGas = await contract.estimateGas.contribute(campaignId, {
      value: parseEther(amount)
    });
    
    return {
      conservative: Math.floor(baseGas.toNumber() * 1.2),
      standard: Math.floor(baseGas.toNumber() * 1.05),
      aggressive: baseGas.toNumber()
    };
  } catch (error) {
    // Fallback to historical averages when estimation fails
    return getHistoricalGasAverages('contribute');
  }
};

Web3 Frontend: The Transaction Lifecycle

The most critical part of our gas optimization wasn't in the contracts — it was in how we handled transaction failures in the UI. When a transaction fails due to insufficient gas, users need immediate feedback and recovery options.

We built a transaction queue system that monitors pending transactions and automatically retries with higher gas when appropriate:

const handleFailedTransaction = async (txHash: string, error: any) => {
  if (error.code === 'UNPREDICTABLE_GAS_LIMIT') {
    // Network congestion - offer to retry with higher gas
    const newGasEstimate = await getHighPriorityGasPrice();
    return showRetryModal(txHash, newGasEstimate);
  }
  
  if (error.code === 'INSUFFICIENT_FUNDS') {
    // User doesn't have enough ETH for gas
    return showFundingOptions();
  }
  
  // Log for analysis
  logTransactionFailure(txHash, error);
};

DeFi Integration: Real-World Performance Numbers

After six months of optimization, here's what we achieved:

  • Campaign creation: 180,000 gas (down from 300,000+)
  • Single contribution: 65,000 gas (down from 85,000)
  • Batch contributions: 45,000 gas per campaign (65% savings for multiple backs)
  • Transaction success rate: 94% (up from 78%)

The success rate improvement came entirely from better gas estimation and retry logic, not contract changes.

The Path of Least Regret

If I were starting over today, here's my exact approach:

  1. Start with gas reports from day one — use hardhat-gas-reporter and set gas cost alerts
  2. Design for batch operations — even if your MVP doesn't need them
  3. Pack your structs — the 15 minutes spent on storage layout saves thousands in user costs
  4. Build transaction retry logic — failed transactions kill user experience faster than high gas costs
  5. Test on multiple networks — Polygon and Arbitrum have different optimization patterns

Don't optimize prematurely, but don't ignore gas costs until users complain. The sweet spot is building gas-conscious patterns from the start without over-engineering.

The Bottom Line

EVM gas optimization is ultimately about user experience, not clever programming. The most elegant contract optimization means nothing if your users can't afford to use your dApp or if failed transactions leave them frustrated and confused.

Focus on the big wins first: storage layout, batch operations, and robust transaction handling. These three changes will give you 80% of your gas savings with 20% of the effort. Everything else is optimization theater.

The current trend toward just-say-no engineering might be shifting, but gas optimization isn't going anywhere. Users will always prefer paying less for transactions, and the networks that make this easiest will win in the long run.

Build your contracts like every user is paying with their own money — because they are.

Have Questions or Need Help?

Our team is ready to assist you with your project needs.

Contact Us