On-chain NFT auctions are expensive because everything that should be ephemeral (a listing, a bid, a cancellation) gets paid for at on-chain prices. I built an NFT marketplace where the hot path is signed off-chain and only settles on-chain at the moment of sale. This is what came out the other side: roughly an order of magnitude cheaper on the user-facing operations and surprisingly tractable to reason about.
The Challenge
Traditional NFT marketplaces require on-chain operations for every listing, bid, and sale. Each interaction costs gas, and with Ethereum's fluctuating gas prices, this can become prohibitively expensive for users.
Our goal was to create a marketplace that:
- Minimizes on-chain transactions
- Maintains security and trustlessness
- Provides excellent user experience
- Scales efficiently
The Solution: Hybrid Architecture
We implemented a hybrid architecture that combines:
1. Off-Chain Coordination
Instead of storing all auction data on-chain, we use off-chain coordination for:
- Listing creation
- Bid placement
- Auction metadata
This data is signed cryptographically and verified on-chain only when necessary.
2. Cryptographic Signatures (ECDSA)
Using Ethereum's ECDSA signature scheme, we enable:
function verifySignature(
address nftContract,
uint256 tokenId,
uint256 price,
bytes memory signature
) internal view returns (bool) {
bytes32 messageHash = keccak256(
abi.encodePacked(nftContract, tokenId, price)
);
bytes32 ethSignedMessageHash = messageHash.toEthSignedMessageHash();
return ethSignedMessageHash.recover(signature) == owner;
}
This allows users to create and sign listings without any on-chain transaction.
3. Batch Settlement
Instead of processing each transaction individually, we batch settlements:
- Reduces gas costs by ~40% per transaction
- Enables atomic swaps of multiple NFTs
- Optimizes storage operations
Key Optimizations
Storage Optimization
Before:
struct Auction {
address seller;
address nftContract;
uint256 tokenId;
uint256 startPrice;
uint256 currentBid;
address currentBidder;
uint256 endTime;
bool active;
}
After:
struct Auction {
address seller; // 160 bits
uint96 currentBid; // 96 bits (fits in same slot)
address nftContract; // 160 bits
uint96 startPrice; // 96 bits (fits in same slot)
uint256 tokenId;
uint32 endTime; // 32 bits
bool active; // 8 bits (packed with endTime)
}
By packing variables deliberately, the struct dropped from 8 slots to 4: a 50% reduction in storage costs per auction.
Event Optimization
Replace expensive storage with indexed events for historical data:
event BidPlaced(
address indexed bidder,
uint256 indexed auctionId,
uint256 amount,
uint256 timestamp
);
Results
Our optimizations yielded impressive results:
- 90% reduction in gas costs for listing creation
- 75% reduction in gas costs for bid placement
- 60% reduction in gas costs for settlements
- Average transaction cost: ~$2-5 (vs $20-50 for traditional marketplaces)
Trade-offs
The win on cost shows up immediately. The cost shows up later: you now have off-chain infrastructure to run, a signature-verification surface that has to be defended at the contract layer, and a signed-listing format that the marketplace and the wallet both have to agree on. The implementation is meaningfully more complex than a vanilla on-chain auction, and the security review has to cover both the on-chain settlement code and the off-chain signing flow. If you're optimizing for engineering simplicity over user gas spend, this is the wrong design. If you're building anything that touches a long-tail of small-value listings, it pays back fast.
Best Practices
Based on our experience, here are key recommendations:
- Pack storage variables - Every storage slot counts
- Use events for historical data - Don't store what you can emit
- Batch operations - Combine multiple actions when possible
- Leverage signatures - Move coordination off-chain
- Test gas usage - Use Foundry's gas reports extensively
- Profile before optimizing - Measure to find the real bottlenecks
Testing with Foundry
We used Foundry extensively for gas profiling:
forge test --gas-report
This helped us identify expensive operations and track improvements over time.