AuctionManager Diamond Proxy & WebUI — Recent Work
AuctionManager Diamond Proxy & WebUI — Recent Work
The AuctionManager suite is a full-featured NFT marketplace built on the EIP-2535 Diamond proxy pattern. It supports English and Dutch auctions for both ERC-721 and ERC-1155 tokens, sale listings, offers, treasury fee splits, and a per-class reward model. The recent work covered the contract architecture, a per-chain admin/dev webui, and the deployment pipeline. This post walks through what was built and why.
The Diamond Proxy Pattern
The original monolithic AuctionManager exceeded Ethereum's 24,576-byte contract size limit (EIP-170). The Diamond pattern splits the system into a lightweight router plus independent facets — each facet is a standalone contract holding a subset of the logic, while all facets share one diamond storage slot. Calls route through the diamond's fallback(), which does a delegatecall to the facet registered for that function selector.
The router (AuctionManagerDiamond) handles:
diamondCut()— add/replace/remove facet selectors, gated by owner or the admin listgetFacets()/facet()/diamondCutLength()— discovery queries for the frontendreceive()/onERC1155Received()— escrow acceptance for ERC-1155 lots_authorizeUpgrade()— UUPS upgrade gate (owner only)
Key design point: shared facets share code, never storage. delegatecall executes facet bytecode against the calling proxy's storage, so each proxy gets its own independent state even when multiple proxies point at the same facet addresses.
Facet Split
The suite is organised into five facets plus a shared storage library:
- Core (
AuctionManagerCore) — initialisation, ownership, pausable/reentrancy guards, fee percentages, admin list, and the core read helpers (getHighBid,buyerFeeForBid) - English (
AuctionManagerEnglish) — English auction lifecycle:createAuction,submitNewBid,finalizeAuction,finalizeEarly, reserve-met auto-finalise, outbid refunds, ERC-2981 royalty splits, and analytics getters (getFloorPrice,getTotalVolume,getGlobalTotalVolume) - Dutch (
AuctionManagerDutch) — linear price decay from start to reserve, active-Dutch tracking - ERC-1155 (
AuctionManagerERC1155) — per-lot quantity auctions, sale listings with quantity, batch-create with atomic escrow, Dutch pricing, offer management, and per-volume-type tracking - TokenRescueFacet — admin-only rescue of stuck ERC-721/ERC-1155 tokens from a discarded proxy
The shared library (LibDiamond) holds the DiamondStorage struct, the slot-key derivation helpers (k256AddrUint, k256AddrAddr), and listing/volume/floor helpers. All nested mappings were replaced with flat bytes32 keys to cut SLOAD gas — for example auctionBids[keccak256(nftContract, tokenId)] instead of auctionBids[nftContract][tokenId].
Storage Layout & Flat Keys
The DiamondStorage struct lives at a fixed slot derived from keccak256("diamond.storage") - 1. It contains all state: counters, fee config, admin list, auction mappings, ERC-1155 balances, sale listings, offers, trades, and analytics. Because the diamond is the only contract that ever initializes this slot, no facet accidentally overwrites another's data.
Key mappings use pre-computed keys:
auctionBids[ keccak256(contract, tokenId) ] → AuctionCore
auctionBidMeta[ keccak256(contract, tokenId) ] → AuctionMeta
saleListings[ keccak256(contract, tokenId) ] → SaleListing
saleListingHistory[ key ] → uint256[] (capped at 20, most recent)
saleListingsById[ listingId ] → SaleListing
currentlyRunning[ ] → uint256[] (active auction IDs)
dutchCurrentPrice[ auctionId ] → uint256 (price at auction start for decay)
contractFloorPrice[ contract ] → uint256 (lowest ask, updated on new listings)
contractTotalVolume[ contract ] → uint256 (sum of all sales)
contractVolumeByType[ contract ][ type ] → uint256 per VenueType (SALE_LISTING, AUCTION_ENGLISH, etc.)This flat structure not only reduces gas but also enables efficient batch reads from the frontend via multicall. The Diamond proxy can return multiple values in a single call.
ERC-1155 Quantity Model
The suite adopted a per-lot quantity model (designated D1/D4 in the decisions log): one class per listing, any quantity the holder owns. Creating an auction for 25 Gold units sells all 25 to the single winner, atomic and trustless. The same principle applies to sale listings: createERC1155SaleListing locks a chosen quantity, and acceptERC1155SaleListing
Bid pricing is per lot, not per unit. The minimum next bid increments by a percentage of the current bid (default 5%), ensuring a healthy bidding progression. Reserve prices work alongside the "buy it now" feature via finalizeOnReserveMet.
Per-Chain Diamond Proxy
The deploy script reads the chainid→RPC map from the webui's chain config at runtime, writes state to the info directory, and preserves the same source while working copies live in the work directory.
The useAuctionManager hook dynamically selects the correct proxy based on the wallet's connected chain. If no chain-specific address exists, it falls back to the Anvil testnet deployment. This enables seamless multi-network support in the admin webui without configuration duplication.
Admin & Dev WebUI
The admin interface is a Vite + React + TanStack Router + wagmi + RainbowKit SPA that loads contract ABIs from a generated manifest (built from the Foundry compiled output), and the hook resolves which facet owns each function.
The useAuctionManager hook centralises all contract interaction:
- Function routing — every function name maps to its owning facet. For example,
createAuction→English,getCurrentDutchPrice→Dutch,createERC1155SaleListing→ERC1155,diamondCut→Diamond - Read/write separation — derived automatically from ABI
stateMutability: view/pure calls useeth_callvia the public client; everything else is sent as a transaction via the connected wallet - Per-chain proxy — a
chainAddrMapin the manifest lets the hook follow the wallet across Anvil, Flare, Coston2, and Songbird without manual configuration - Auto-bound methods — every entry in
FN_ROUTESis exposed as a convenience wrapper, plus genericcallRead/callWritefor ad-hoc calls
The AuctionPanel component renders all of this into a usable UI: create auctions, submit bids, finalise (early or normally), view active listings, and see real-time countdowns synced to the chain clock (not the browser clock). Approval is handled transparently — if the NFT isn't approved for the proxy, the panel triggers an approve call, waits for confirmation, then proceeds to create the auction. The ERC-1155 batch flow works the same way, with a single atomic safeBatchTransferFrom escrowing all lot quantities.
Deployment Pipeline
Deployment uses a single shell script — deploy-all-auction-contracts.sh — that runs end-to-end:
- Build all five facet contracts plus the ERC1967 proxy implementation via Foundry
- Deploy each facet using
forge createwith the configured signer, auto-copying each ABI into the abis directory (archiving any previous version to the legacy subdirectory) - Extract function selectors from each compiled ABI using
cast sig - Build
diamondCutcalldata — a Python helper encodes theFacetCut[]array, mapping every selector to its facet address with theAddaction - Deploy the Diamond proxy — an ERC1967 proxy with the Diamond router as implementation and
initialize()as the constructor init data - Call
diamondCut()on the proxy to wire all facets - Initialise each facet —
initialize()called on the proxy for Core, English, Dutch, and ERC1155 facets (the router's owninitializeseeds the shared LibDiamond storage) - Verify — read diamond storage slot,
diamondCutLength,getFacets(),getActiveAuctionIDs(), and proxy owner - Write state to a chainid-suffixed state file
The script selects the target chain via CHAINID (looked up in the chain map from the webui's chain config), with PRIVATEKEY overriding DEPLOYER_KEY. The state file is chainid-suffixed so deployments to different networks never collide.
Key Decisions & Lessons
Several principles guided the implementation:
- Size matters — factories must fit under EIP-170's 24,576-byte limit. The assembly-optimised
1155_asm.solachieves 24,307 bytes (viaIR off, runs=1), while the 721 factory ships at ~23,477 bytes. Storage/config is deliberately kept out of the factory and moved intoNFTConfiguratorto keep bytecode lean. - Storage is append-only — appending fields to a struct is safe; reordering or inserting mid-struct breaks old offsets. A genuinely new layout needs a new slot-key, not an edit to an old one.
- Admin is not the bottleneck — splitting only admin functions saves negligible size. The real weight is in the business paths (payable mints, multi-struct configs, overloaded hooks). The Diamond's shared
FacetRegistrymatters more for fleet-wide upgrade cost (onereplaceSelectorvs updating N per-collection maps). - Gas analytics are off-chain — floor price, average sale price, and time-series metrics are computed by the backend from on-chain events and served to the frontend. The contract keeps only a cheap per-contract floor mapping.
- Frontend follows the chain — the webui's proxy resolution is derived from the connected wallet's chainid, keeping the admin experience consistent across testnet and production.
The work demonstrates that the Diamond pattern is not just a theoretical exercise — it delivers real upgradeability and gas advantages for a production marketplace. By separating concerns into facets, the suite can evolve individual functions without redeploying the entire system, while the per-chain webui makes the developer experience seamless across networks.