Smart Contract Examples: Escrow, Dutch Auction, and Staking Vault

An escrow smart contract holds money until an agreed condition is met, then releases it to the right party — no bank, and no person sitting in the middle holding the funds. It’s one of the clearest ways to see what Solidity actually does, which is why it leads the three worked examples below.

Reading a short contract teaches more than any definition. Each of the three here is small enough to follow top to bottom, and each one solves a real problem: holding payment safely, selling something at a falling price, and pooling deposits into a shared vault. The patterns show up across most of DeFi once you know what to look for.

Everything compiles on recent tooling — Solidity 0.8.36, the current release as of mid-2026, and OpenZeppelin Contracts 5.x. One thing has changed that trips people up: OpenZeppelin removed its old escrow contracts in version 5.0, so the first example builds one from primitives that still ship.

What Is a Smart Contract?

A smart contract is a program stored on a blockchain that runs exactly as written whenever someone calls it. Ethereum’s documentation describes it as code and data living at a specific address, with no way for anyone to quietly change the rules after deployment.

That permanence is the whole point. Once a contract is live, its logic is fixed unless you built in an upgrade path beforehand, and everyone can read the source. The flip side is unforgiving: a bug you ship is a bug that stays, which is why the examples here lean on audited building blocks and simple state machines rather than clever code.

What You Need to Follow Along

Three pieces cover everything below. A compiler and environment — Remix in the browser for a quick read, or Hardhat and Foundry locally for anything you’ll test and redeploy. OpenZeppelin Contracts as a dependency, since two of the three examples inherit from it. And an endpoint to broadcast the deployment, which the last section covers.

None of this costs real money until you choose mainnet. Testnet ETH is free, and deploying to a test network first is the normal way to confirm a contract behaves before it touches live funds.

Example 1: An Escrow That Releases Funds on a Condition

Escrow means a third party holds value until the terms of a deal are satisfied. On-chain, that third party is code. The contract takes custody of ETH, waits for an agreed signal, and then pays out — either forward to the recipient or back to the sender.

This escrow smart contract names three roles. The buyer deposits the funds, the seller receives them on success, and the arbiter is a neutral party who can release the payment or refund it if the deal falls apart. The buyer can also release early once satisfied.

solidity

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

import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/// @title Minimal ETH escrow with a neutral arbiter
contract Escrow is ReentrancyGuard {
    enum State { AwaitingPayment, Funded, Released, Refunded }

    address public immutable buyer;
    address public immutable seller;
    address public immutable arbiter;
    State public state;

    constructor(address _seller, address _arbiter) payable {
        buyer = msg.sender;
        seller = _seller;
        arbiter = _arbiter;
        state = msg.value > 0 ? State.Funded : State.AwaitingPayment;
    }

    function deposit() external payable {
        require(msg.sender == buyer, "only buyer");
        require(state == State.AwaitingPayment, "already funded");
        require(msg.value > 0, "no value");
        state = State.Funded;
    }

    function release() external nonReentrant {
        require(msg.sender == arbiter || msg.sender == buyer, "not authorized");
        require(state == State.Funded, "not funded");
        state = State.Released;
        (bool ok, ) = seller.call{value: address(this).balance}("");
        require(ok, "transfer failed");
    }

    function refund() external nonReentrant {
        require(msg.sender == arbiter, "only arbiter");
        require(state == State.Funded, "not funded");
        state = State.Refunded;
        (bool ok, ) = buyer.call{value: address(this).balance}("");
        require(ok, "transfer failed");
    }
}

Two details carry most of the safety. Each payout sets the state before sending ETH — the checks-effects-interactions pattern that stops a malicious recipient from re-entering the contract mid-transfer. The nonReentrant guard from OpenZeppelin is a second layer on top of that.

A smart contract escrow like this fits any deal where neither side fully trusts the other. Freelance marketplaces hold a client’s payment until work is delivered. Over-the-counter desks park one asset while the counterparty sends the other. The arbiter can be a person, a multisig, or another contract — the custody logic doesn’t care which.

Example 2: A Dutch Auction That Lowers Its Own Price

A Dutch auction starts high and falls over time until someone buys. The seller sets a starting price, a duration, and a discount rate; the price drops every second until the first buyer accepts whatever it reads at that moment. Cyfrin’s reference implementation is the pattern most NFT drops copy.

The math is one line: current price equals the starting price minus the discount rate multiplied by the seconds elapsed. The contract below sells a single ERC-721 NFT and refunds any ETH a buyer overpays.

solidity

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

interface IERC721 {
    function transferFrom(address from, address to, uint256 id) external;
}

contract DutchAuction {
    uint256 public immutable startPrice;
    uint256 public immutable discountRate; // wei per second
    uint256 public immutable startAt;
    uint256 public immutable endAt;
    address payable public immutable seller;
    IERC721 public immutable nft;
    uint256 public immutable nftId;

    constructor(
        uint256 _startPrice,
        uint256 _discountRate,
        address _nft,
        uint256 _nftId,
        uint256 _duration
    ) {
        require(_startPrice >= _discountRate * _duration, "start price too low");
        seller = payable(msg.sender);
        startPrice = _startPrice;
        discountRate = _discountRate;
        startAt = block.timestamp;
        endAt = block.timestamp + _duration;
        nft = IERC721(_nft);
        nftId = _nftId;
    }

    function price() public view returns (uint256) {
        uint256 elapsed = block.timestamp - startAt;
        return startPrice - (discountRate * elapsed);
    }

    function buy() external payable {
        require(block.timestamp < endAt, "auction ended");
        uint256 current = price();
        require(msg.value >= current, "not enough ETH");

        nft.transferFrom(seller, msg.sender, nftId);

        uint256 refund = msg.value - current;
        if (refund > 0) {
            (bool ok, ) = payable(msg.sender).call{value: refund}("");
            require(ok, "refund failed");
        }
        (bool paid, ) = seller.call{value: current}("");
        require(paid, "pay seller failed");
    }
}

The constructor check matters more than it looks. Requiring startPrice >= discountRate * duration guarantees the price can’t fall below zero during the valid window — without it, a long enough auction would underflow and revert on every call. Solidity 0.8 reverts on underflow by default, so the guard turns a confusing failure into a clear deployment-time error.

Falling-price sales suit situations where the seller doesn’t know what the market will bear. NFT collections use them to avoid gas wars at a fixed mint price. Protocols use them to liquidate collateral or distribute tokens at a rate buyers discover themselves. The trade-off is timing: wait too long for a lower price and someone else takes it first.

Example 3: A Staking Vault Built on ERC-4626

A vault takes deposits, pools them, and hands each depositor a share token that tracks their slice of the pool. As the pool earns yield, each share is redeemable for more of the underlying asset than it cost. ERC-4626 is the standard that makes every vault speak the same interface, so a wallet or aggregator can integrate one vault the same way it integrates the next.

OpenZeppelin ships a tested ERC4626 base, which turns a working vault into a few lines. Inherit it, name the share token, and pass the asset the vault accepts.

solidity

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

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";

/// @title A minimal tokenized staking vault
contract StakingVault is ERC4626 {
    constructor(IERC20 asset_)
        ERC20("Staked Token", "sTKN")
        ERC4626(asset_)
    {}

    // Widen the virtual-share offset to harden against inflation attacks
    function _decimalsOffset() internal pure override returns (uint8) {
        return 6;
    }
}

The one override earns its place. New vaults are vulnerable to an inflation attack, where an attacker front-runs the first real depositor, donates assets directly to the vault to skew the share price, and walks off with most of the next deposit. OpenZeppelin defends against this with virtual shares and a decimal offset that makes the attack cost far more than it could ever return. As one OpenZeppelin maintainer put it on the project forum, because of those virtual shares “there cannot ever be a situation where there are 0 shares.” Raising _decimalsOffset() pushes that cost higher still.

Vaults are the backbone of yield in DeFi. Staking pools, lending markets, and auto-compounding strategies all expose a vault so users can deposit once and let the position grow. Standardizing the interface is why a new strategy can plug into existing frontends on day one instead of asking every integrator to write custom code.

How the Three Examples Compare

Different jobs, same underlying idea: a contract takes custody of value and releases it under rules nobody can quietly rewrite.

EscrowDutch auctionStaking vault (ERC-4626)
What it holdsETH until a condition clearsAn NFT listed for saleDeposited assets, in exchange for shares
What triggers payoutArbiter or buyer releasingA buyer accepting the current priceA holder redeeming shares
Main risk to watchReentrancy, trust in the arbiterArithmetic underflow, front-runningInflation attack, rounding
Base to build onCustom (OpenZeppelin removed its escrow)CustomOpenZeppelin ERC4626 base

Deploying and Testing These Contracts

None of these tools talk to Ethereum on their own — each one hands a signed transaction to an endpoint and waits for a reply. You can run that endpoint yourself by syncing a full client, or use a provider and skip the maintenance. Through NOWNodes, the same account covers both Ethereum mainnet and the Sepolia testnet.

The practical path is Sepolia first. Point your tool at https://eth-sepolia.nownodes.io with your API key in an api-key header, deploy, and exercise every function with test ETH before spending anything real. Our Start plan covers 100,000 requests at no cost, which is more than enough to compile, deploy, and poke at all three contracts several times over. Switching to mainnet is a one-line change to https://eth.nownodes.io.

Teams building through an AI coding assistant rather than hand-written scripts can reach the same methods through our MCP server, which connects tools like Claude Code and Cursor to the endpoint directly.

Security Patterns Worth Internalizing

The same handful of mistakes account for most losses, and all three examples above are shaped around avoiding them. Reentrancy drained early DeFi before checks-effects-interactions and reentrancy guards became standard. Weak access control — a payout anyone can call, a mint with no owner check — remains one of the most common audit findings. Rounding and first-depositor edge cases sink vaults that skip the virtual-share defense.

The numbers keep the stakes honest. Attackers stole more than $3.4 billion in crypto during 2025, according to Chainalysis — the worst year since 2022. The more encouraging detail: hacks targeting DeFi protocols stayed low even as the value locked in them recovered, which Chainalysis called “a meaningful change.” Hardened patterns and audited libraries are a large part of why.

That’s the case for treating the examples here as starting points, not production code. Anything holding real value deserves tests against the edge cases and, for a live deployment, an independent review. An escrow smart contract is only ever as trustworthy as the code behind it.

Conclusion

These three contracts cover a surprising amount of ground. Escrow shows conditional custody and safe payouts, a Dutch auction shows time-based pricing and refunds, and an ERC-4626 vault shows pooled deposits with a standardized interface. Each is short on purpose — the ideas are reusable, the line count isn’t.

Start on Sepolia, inherit from audited bases where one exists, and resist the urge to be clever in code that holds funds. Read each example until the state transitions make sense, deploy them, and break them on a testnet. That’s the fastest way to go from recognizing these patterns to writing your own.

FAQ

Are These Smart Contract Examples Safe to Use in Production?

Treat them as teaching code, not production code. They’re deliberately minimal and skip features real deployments need — fee logic, pausing, thorough edge-case tests. For anything holding real value, test extensively and get an independent audit before mainnet.

Can These Contracts Run on Chains Other Than Ethereum?

Yes, on any EVM-compatible network. BNB Smart Chain, Polygon, Arbitrum, Base, and others run the same bytecode Solidity compiles to, so the contracts deploy unchanged — only the endpoint and gas costs differ.

How Much Does It Cost to Deploy an Example Contract?

Nothing on a testnet, since test ETH is free. On mainnet, cost depends on gas prices and contract size, but with fees low compared to past years, deploying a small contract like these typically costs a fraction of a dollar rather than the double digits seen during past congestion.

How Is a Dutch Auction Different From a Regular Auction?

A standard (English) auction starts low and climbs as bidders compete, ending with the highest bid. A Dutch auction runs the opposite way — it starts high and the price