Skip to main content

Web3 Development - Lessons Learned in 2023

· 13 min read
RedDemonFox
Software Developer & Tech Enthusiast

Three years ago, I dove headfirst into Web3 development. Like many developers at the time, I was captivated by the promise of decentralization, trustless systems, and the ability to build applications that weren't controlled by centralized gatekeepers. The hype was real, and the funding was flowing.

Fast forward to mid-2023, and the landscape has changed dramatically. After the crashes, the hacks, and the regulatory scrutiny, many fair-weather Web3 developers have moved on to the next shiny technology. But for those of us who stuck around, the past year has been incredibly educational. We've moved from the wild speculation phase to a more mature, pragmatic approach to blockchain development.

In this post, I'll share what I've learned building Web3 projects through the ups and downs of the market cycle. These lessons weren't easy to learn—many came from painful failures, security incidents, and shifting client requirements—but they've made me a better blockchain developer.

Lesson 1: Smart Contract Security is a Different Beast

In traditional web development, security vulnerabilities can often be patched after deployment. In smart contract development, that mindset will get you rekt. Once deployed, smart contracts are immutable by default, and any vulnerability can be immediately exploited.

The Painful Lesson

Last October, I helped audit a DeFi protocol before launch. Despite multiple reviews, a subtle reentrancy vulnerability made it through to production. Within hours of deployment, attackers drained $2.3 million from the protocol's liquidity pools.

The vulnerability wasn't obvious—it wasn't a direct reentrancy on the same function but a cross-function reentrancy that only occurred when multiple contracts interacted in a specific way. Even experienced Solidity developers missed it during review.

The Better Approach

After that incident, we completely revamped our development process:

  1. Multiple layers of auditing - Internal review, automated tools (Slither, Mythril), and at least two independent professional audits for any contract handling significant value

  2. Formal verification where possible - For critical functions, using formal verification tools like Certora to mathematically prove correctness

  3. Testnet deployments with bounties - Deploying to testnet weeks before mainnet and offering significant bounties for vulnerabilities found

  4. Incremental deployment - Starting with lower value caps and gradually increasing them as the contract proves itself secure

Here's a snippet of a secure withdrawal function that applies some of these lessons:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract SecureVault is ReentrancyGuard, Ownable {
mapping(address => mapping(address => uint256)) private _balances;

event Withdrawal(address indexed token, address indexed user, uint256 amount);

// Prevent reentrancy attacks with the nonReentrant modifier
function withdrawToken(address token, uint256 amount) external nonReentrant {
// Check
require(amount > 0, "Amount must be greater than zero");
require(_balances[token][msg.sender] >= amount, "Insufficient balance");

// Effect
_balances[token][msg.sender] -= amount;

// Interaction (after state changes)
bool success = IERC20(token).transfer(msg.sender, amount);
require(success, "Transfer failed");

emit Withdrawal(token, msg.sender, amount);
}

// Additional functions...
}

The pattern of separating checks, effects, and interactions is crucial in smart contract development. This, combined with OpenZeppelin's ReentrancyGuard, provides protection against the most common attack vectors.

Lesson 2: Gas Optimization is an Art Form

When I started in Web3, I treated gas optimization as an afterthought. As Ethereum gas prices soared, this approach quickly became untenable. Users simply won't use your dApp if every interaction costs $50+ in gas fees.

The Hard Reality

A governance DAO I built in early 2022 worked perfectly in testing, but when deployed to mainnet during high gas prices, voting on proposals cost users over $100 per vote. Participation plummeted, essentially killing the DAO's effectiveness. It was a classic case of technical success but practical failure.

Strategies That Work

Gas optimization is now a core consideration from day one of development, not a final optimization step. Here's what's worked:

  1. Struct packing - Carefully ordering struct variables to fit into as few storage slots as possible

  2. Batch operations - Allowing users to perform multiple operations in a single transaction

  3. State caching - Minimizing SLOADs by caching state variables in memory during function execution

  4. Using events over storage - Storing historical data in events rather than contract storage

  5. Off-chain computation - Moving as much computation off-chain as possible, using techniques like merkle proofs for verification

Here's a simple example of struct packing, which can save significant gas:

// Unoptimized struct: Uses 3 storage slots (each slot is 32 bytes)
struct UserInfo {
uint256 balance; // 32 bytes
address userAddress; // 20 bytes
bool isActive; // 1 byte
}

// Optimized struct: Uses 2 storage slots
struct UserInfo {
uint256 balance; // 32 bytes
bool isActive; // 1 byte (packed with userAddress)
address userAddress; // 20 bytes
}

This might seem like a small change, but when you're storing thousands of these structs, the gas savings add up quickly.

Lesson 3: User Experience Can't Be an Afterthought

The biggest barrier to Web3 adoption isn't the technology—it's the user experience. Asking users to install a wallet, manage private keys, approve transactions, and pay gas fees creates significant friction compared to traditional web applications.

What Users Really Want

After launching several dApps with varying levels of success, I've found that users don't care about decentralization for its own sake. They care about the benefits it provides: ownership of their assets, financial inclusion, censorship resistance, and new economic opportunities.

If your application doesn't provide these benefits in a user-friendly way, users will stick with Web2 alternatives.

UX Improvements That Made a Difference

  1. Account abstraction - Implementing EIP-4337 to create smart contract wallets with features like social recovery, gasless transactions, and batch transactions

  2. Gas abstraction - Using meta-transactions or gas stations to sponsor user transactions, especially for onboarding

  3. Progressive decentralization - Starting with a more familiar, centralized experience and gradually introducing decentralized elements as users become comfortable

  4. Better transaction UX - Clear explanations of what each transaction does, estimated costs, and detailed error messages when things go wrong

Here's a simple example of implementing EIP-712 signatures for gasless transactions:

// On the client side
async function createMetaTransaction(signer, account, contractAddress, functionData) {
const domain = {
name: 'Your dApp Name',
version: '1',
chainId: 1, // Ethereum mainnet
verifyingContract: contractAddress
};

const types = {
MetaTransaction: [
{ name: 'from', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'functionData', type: 'bytes' }
]
};

const nonce = await contract.getNonce(account);

const value = {
from: account,
nonce: nonce,
functionData: functionData
};

const signature = await signer._signTypedData(domain, types, value);
return { account, nonce, functionData, signature };
}

// This can be submitted to a relayer that pays for gas
// The contract validates the signature and executes the function

When implemented correctly, this allows users to interact with your dApp without having ETH for gas, dramatically improving the new user experience.

Lesson 4: Protocol Design Matters More Than Implementation

In the early days of my Web3 journey, I focused mainly on the technical implementation of smart contracts. I've since learned that while clean, secure code is necessary, it's not sufficient. Protocol design—the economic and game theory aspects of your system—often determines success or failure.

Design Flaws That Killed Good Projects

I worked with a team building a lending protocol with what seemed like a solid technical implementation. But the protocol had a fundamental design flaw: the liquidation mechanism didn't properly account for extreme market volatility. When a market crash happened, liquidators couldn't keep up, and the protocol became insolvent.

The code worked exactly as written, but the protocol design didn't align incentives correctly for all market conditions.

Protocol Design Principles That Work

  1. Mechanism design from first principles - Start with clear goals and build mechanisms that align all participants' incentives

  2. Agent-based simulation - Test protocol economics with agent-based simulations that model different participant strategies and market conditions

  3. Gradual parameter adjustment - Launch with conservative parameters and adjust gradually based on real-world usage

  4. Circuit breakers - Design mechanisms that automatically pause or limit certain activities during extreme conditions

  5. Focus on sustainable value capture - Design tokenomics around sustainable value capture rather than inflationary rewards

Here's a conceptual example of a circuit breaker in a DeFi protocol:

// Circuit breaker for a lending market
function borrow(address asset, uint256 amount) external whenNotPaused {
// Regular borrowing logic...

// Check for unusual market conditions after the borrow
uint256 utilizationRate = calculateUtilizationRate(asset);

// If utilization gets too high, start scaling down max borrow amounts
if (utilizationRate > safeUtilizationThreshold) {
// Reduce the max borrow amount for subsequent borrows
uint256 reductionFactor = calculateReductionFactor(utilizationRate);
maxBorrowAmount[asset] = maxBorrowAmount[asset] * reductionFactor / PRECISION;
}

// If utilization hits critical levels, pause borrowing entirely
if (utilizationRate > criticalUtilizationThreshold) {
_pauseBorrowing(asset);
emit BorrowingPaused(asset, block.timestamp);
}
}

These kinds of circuit breakers can prevent catastrophic failures during extreme market conditions.

Lesson 5: The Future is Multi-Chain

I started as an Ethereum maximalist, believing that all meaningful blockchain activity would eventually consolidate on Ethereum or its L2s. The past year has changed my perspective—the future of Web3 is undoubtedly multi-chain.

Cross-Chain Realities

A gaming project I worked on initially launched exclusively on Ethereum, but gas costs made simple in-game actions prohibitively expensive. We eventually deployed on multiple chains (Ethereum, Polygon, and Immutable X), which created a much better user experience but significantly increased development complexity.

Making Multi-Chain Work

  1. Abstract blockchain interactions - Build applications with a blockchain abstraction layer that handles the differences between chains

  2. Focus on cross-chain identity - Use solutions like ENS or Lens Protocol to maintain consistent user identity across chains

  3. Thoughtful state management - Design systems where critical state is maintained on the most secure chain, while high-frequency interactions happen on more scalable chains

  4. Use cross-chain messaging protocols - Implement bridges like Axelar, LayerZero, or Hyperlane for secure cross-chain communication

Here's a simplified example of a blockchain abstraction layer in JavaScript:

class BlockchainService {
constructor() {
this.providers = {
ethereum: new ethers.providers.JsonRpcProvider(ETH_RPC_URL),
polygon: new ethers.providers.JsonRpcProvider(POLYGON_RPC_URL),
arbitrum: new ethers.providers.JsonRpcProvider(ARBITRUM_RPC_URL)
};

this.contracts = {
ethereum: new ethers.Contract(ETH_CONTRACT_ADDRESS, ABI, this.providers.ethereum),
polygon: new ethers.Contract(POLYGON_CONTRACT_ADDRESS, ABI, this.providers.polygon),
arbitrum: new ethers.Contract(ARBITRUM_CONTRACT_ADDRESS, ABI, this.providers.arbitrum)
};
}

async getUserBalance(address, chain = 'ethereum') {
try {
const contract = this.contracts[chain];
return await contract.balanceOf(address);
} catch (error) {
console.error(`Error fetching balance on ${chain}:`, error);
throw error;
}
}

async transferTokens(from, to, amount, chain = 'ethereum') {
try {
const signer = this.providers[chain].getSigner(from);
const contract = this.contracts[chain].connect(signer);
const tx = await contract.transfer(to, amount);
return await tx.wait();
} catch (error) {
console.error(`Error transferring on ${chain}:`, error);
throw error;
}
}

// Additional cross-chain methods...
}

This approach allows your application to work across multiple chains while providing a consistent API for your frontend.

Lesson 6: Regulatory Clarity is Coming (Ready or Not)

When I started in Web3, regulatory considerations were barely an afterthought. The space operated under a "move fast and ask forgiveness later" mentality. That era is definitively over.

The Changing Landscape

Several projects I consulted for had to significantly pivot or even shut down operations in certain jurisdictions due to regulatory concerns. What was once a gray area is increasingly becoming black and white, and the cost of non-compliance can be existential.

Adapting to Regulatory Reality

  1. Jurisdiction-aware development - Building features that can adapt to different regulatory requirements in different regions

  2. Compliance by design - Incorporating KYC/AML where necessary, especially for projects involving financial transactions

  3. Decentralized governance - Implementing truly decentralized governance to reduce regulatory risk

  4. Transparent tokenomics - Ensuring token distributions and mechanisms are transparent and don't resemble securities

Here's a concept for implementing jurisdiction-aware feature access:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "@openzeppelin/contracts/access/Ownable.sol";

contract JurisdictionAwareProtocol is Ownable {
// Mapping of features to their status in each jurisdiction
mapping(bytes32 => mapping(uint256 => bool)) private _featureAvailability;

// Mapping of user addresses to their jurisdiction codes
mapping(address => uint256) private _userJurisdictions;

event JurisdictionUpdated(address user, uint256 jurisdictionCode);
event FeatureAvailabilityUpdated(bytes32 featureId, uint256 jurisdictionCode, bool isAvailable);

// Set a user's jurisdiction (could be done through a KYC process)
function setUserJurisdiction(address user, uint256 jurisdictionCode) external onlyOwner {
_userJurisdictions[user] = jurisdictionCode;
emit JurisdictionUpdated(user, jurisdictionCode);
}

// Update feature availability for a jurisdiction
function setFeatureAvailability(bytes32 featureId, uint256 jurisdictionCode, bool isAvailable) external onlyOwner {
_featureAvailability[featureId][jurisdictionCode] = isAvailable;
emit FeatureAvailabilityUpdated(featureId, jurisdictionCode, isAvailable);
}

// Check if a feature is available for a user
function isFeatureAvailable(bytes32 featureId, address user) public view returns (bool) {
uint256 jurisdictionCode = _userJurisdictions[user];
return _featureAvailability[featureId][jurisdictionCode];
}

// Use this modifier to restrict function access based on feature availability
modifier whenFeatureAvailable(bytes32 featureId) {
require(isFeatureAvailable(featureId, msg.sender), "Feature not available in your jurisdiction");
_;
}

// Example of a jurisdiction-restricted function
function highYieldStaking() external whenFeatureAvailable("HIGH_YIELD_STAKING") {
// Implementation...
}
}

This pattern allows protocols to adapt to different regulatory requirements without completely rebuilding for each jurisdiction.

Lesson 7: Community is the Most Valuable Asset

Perhaps the most important lesson I've learned is that the true value of Web3 projects lies in their communities. Technology can be copied, but engaged communities are difficult to build and almost impossible to replicate.

Community Building That Works

The most successful project I worked on wasn't the one with the most innovative technology—it was the one that invested heavily in community from day one. They dedicated 30% of their budget to community initiatives, including:

  1. Developer grants - Funding developers to build tools and integrations for the ecosystem

  2. Educational content - Creating accessible learning resources for users at all levels

  3. Transparent governance - Involving the community in key decisions through a well-designed governance process

  4. Regular community calls - Hosting open discussions about the project's direction and challenges

  5. Localized communities - Building presence in key markets with region-specific ambassadors and content

The result was a resilient community that stuck with the project through market downturns and technical challenges.

The Road Ahead

Despite the challenges of the past year, I remain optimistic about Web3's future. The speculative excess has been washed out, leaving behind builders focused on creating real value. The technology is maturing, user experiences are improving, and real-world use cases are emerging.

For developers considering entering the space, my advice is:

  1. Start with fundamentals - Learn the core concepts before diving into the latest frameworks
  2. Build something useful - Focus on solving real problems, not speculative tokenomics
  3. Prioritize security - Assume your code will be attacked from day one
  4. Design for real users - Not just crypto natives
  5. Stay adaptable - The only constant in Web3 is change

Web3 development in 2023 is less about hype and more about building sustainable solutions. It's challenging, sometimes frustrating, but still one of the most exciting spaces in software development today.

What lessons have you learned building in Web3 this year? I'd love to hear your experiences in the comments.


This article draws from my experience building multiple Web3 projects over the past year, including a DeFi lending protocol, an NFT marketplace, and a DAO governance system. For more technical deep dives, check out my previous articles on smart contract security and token engineering.