DUTCH PROTOCOL is an NFT auction platform featuring integrated deflationary tokenomics. The system implements a bonding curve-backed token economy designed to create sustainable value accrual through auction fee monetization and systematic token supply reduction.
- DUTCH token: ERC‑20 with a walled‑garden design – transfers are restricted/allow‑listed so all price discovery flows through the bonding‑curve engine.
- BondingCurve: Standalone contract implementing the bonding‑curve math for WETH/DUTCH, with symmetric buy/sell taxes that fund the protocol. Public
buyDutchExactIn,buyDutchExactOut, andredeemDUTCH. - DutchVault + Marketplace: Vault receives the NPA tax share, accumulates ETH by collection, buys floor NFTs, and lists them on the Dutch auction marketplace with configurable pricing.
- Auction Assist: Let users temporarily co‑fund vault purchases; when NFTs are sold, they receive pro‑rata DUTCH rewards and the protocol burns the rest.
- Presale: Optional ETH presale that mints DUTCH via the same curve, then streams tokens to early participants using Sablier V2.
- Project Description
- Core Components
- Prerequisites/Requirements
- Project Structure
- Dependencies
- Installation/Setup Instructions
- Testing Commands
- License Information
The $DUTCH token serves as the platform's exclusive currency with a controlled trading environment. As an ERC-20 token with 18 decimals, it maintains deflationary properties from launch without venture capital dilution or vesting schedules (unless bought in the public presale).
Component Summary:
- Controlled transfers: Only allow‑listed protocol contracts can freely move tokens when restrictions are enabled.
- Curve‑driven supply: Minting/burning is owned by the bonding‑curve engine; users cannot mint directly.
- User burn: Anyone can send DUTCH to
0xdeadviauserBurnwithout changing the curve’s effective supply, increasing backing per circulating token.
Walled Garden Approach
The contract employs transfer restrictions and blacklist functionality to prevent external DEX trading, ensuring all transactions flow through the bonding curve mechanism. While total blacklisting of all DEXes is not feasible, targeting major exchanges (Uniswap, SushiSwap, etc.) should be sufficient to maintain the walled garden approach and ensure the protocol captures the majority of tax revenue from trading activities.
Supply Management
Supply control is exclusively managed by the bonding-curve engine (BondingCurve) through dual supply tracking:
totalSupply()- circulating tokensbondingCurveSupply()- pricing calculations
In addition, an allowlist of protocol addresses (_allowedTransferors) can always transfer even when global restrictions are turned on, enabling controlled integrations (e.g., vaults, marketplaces, or presale contracts).
User Burn Mechanism
The userBurn() function transfers tokens to burn address (0xdead) without affecting either supply metric, creating treasury surplus and unreachable ETH reserves that strengthen protocol backing.
Administrative Controls:
| Function | Purpose | Access |
|---|---|---|
pause()/unpause() |
Emergency halt of all token operations | Owner only |
setTransfersRestricted() |
Toggle global P2P transfer restrictions | Owner only |
setBlacklisted() |
Add/remove addresses from blacklist | Owner only |
setAllowedTransferor(address,bool) |
Add/remove protocol addresses from the transfer allowlist | Owner only |
transferOwnership()/acceptOwnership() |
Two-step ownership transfer | Owner/Pending Owner |
Key Security Features:
- OpenZeppelin Pausable for emergency stops
- Ownable2Step prevents accidental ownership loss
- Blacklist functionality for compliance
- Transfer restrictions to maintain walled garden
Configuration Options:
| Setting | Function | Parameters | Limits | Default |
|---|---|---|---|---|
| Transfer Restrictions | setTransfersRestricted(bool) |
true/false |
- | false |
| Blacklist Management | setBlacklisted(address, bool) |
wallet address, status | - | None blacklisted |
| Emergency Pause | pause()/unpause() |
- | - | Unpaused |
Single on-chain component (BondingCurve.sol) that implements the bonding curve economics for WETH/DUTCH. It is a standalone contract (no Uniswap or other AMM).
Component Summary:
- Single source of truth for price and supply: all DUTCH minting/burning goes through this contract.
- Symmetric taxes on buys/sells fund DutchVault (NPA) and the ops wallet.
- Slippage protection on public
buyDutchExactIn,buyDutchExactOut, andredeemDUTCH.
Core Function: Manages all DUTCH minting/burning with algorithmic price stability and routes buy/sell taxes to protocol destinations.
Key Features:
| Feature | Details |
|---|---|
| Pricing Model | price = C × supply^1.5 (C = 500,000,000 wei) |
| Reserve Asset | WETH/ETH-only backing with internal balance tracking |
| Tax Structure | 10% buy tax, 10% sell tax (symmetric design) |
| Tax Distribution | 75% → NPA wallet, 25% → Operations wallet |
| Security | ReentrancyGuard + Pausable |
Entry Points:
| Function | Caller | Purpose |
|---|---|---|
buyDutchExactIn(minOut, recipient) |
Anyone | Exact-in: ETH → DUTCH (buy tax unless exempt) |
buyDutchExactOut(exactDutch, recipient) |
Anyone | Exact-out: DUTCH for ETH (buy tax unless exempt) |
redeemDUTCH(amount, minEth) |
Anyone | Sell DUTCH for ETH (sell tax applied) |
Design Purpose:
- Provide on-chain price discovery for DUTCH via the polynomial curve.
- Generate protocol revenue through symmetric buy/sell taxes.
- Fund NFT acquisition via DutchVault and protocol operations via the ops wallet.
Core Operations Flow (High Level):
- Minting path (buys): User or presale/marketplace sends ETH; contract computes tax, routes NPA/ops shares, deposits net WETH into reserve, and mints DUTCH per the integral
price = C × supply^1.5. - Redemption path (sells): User calls
redeemDUTCH; contract burns DUTCH, applies sell tax, and sends net ETH to the user.
Treasury & Actors (Conceptual):
| Actor | Role | Description |
|---|---|---|
| User/Investor | Token Operations | Buys or sells $DUTCH via buyDutchExactIn / buyDutchExactOut / redeemDUTCH |
| Protocol Owner | Governance | Updates fees, tax splits, and destination wallets |
| Contract Reserve | Treasury | Internal WETH/ETH balance backing redemptions (not a wallet) |
| NPA Wallet | NFT Purchase Account | Receives 75% of taxes; accumulates ETH to buy floor-priced NFTs |
| Ops Wallet | Operations | Team-owned wallet receiving 25% of taxes for marketing purposes |
Fund Flow Summary:
| Source | Allocation | Destination | Purpose |
|---|---|---|---|
| Mint ETH | 90% after tax | Contract Reserve | WETH backing for redemptions |
| Buy Tax (10%) | 75% / 25% | NPA / Operations | NFT purchases / Team operations |
| Sell Tax (10%) | 75% / 25% | NPA / Operations | NFT purchases / Team operations |
| Excess Reserves | 100% | NPA Wallet | Additional NFT acquisition funding |
Observability:
getBondingCurveSupply()– effective curve supply (totalMinted - totalBurned).getReserveBalance()– WETH reserve backing the curve.getTotalMinted()/getTotalBurned()– cumulative DUTCH minted and burned.
Smart-contract treasury that receives the NPA (vault) share of bonding‑curve taxes and automates NFT purchasing, listing, and settlement accounting.
Component Summary:
-
Allocates ETH across collections using configurable basis‑point weights.
-
Permissionless buy/list helpers so anyone can trigger NFT purchases and listings within configured risk bounds.
-
Tracks realized PnL and burned DUTCH for full transparency of vault performance.
-
Per‑collection allocation engine:
- Owner configures collections and their weights via
setCollectionAllocations(...)(must sum to 10000 BPS). - Incoming ETH from the bonding curve is split across collections on
receive()according to these weights, with round‑robin dust handling. - Per‑collection balances and base prices are tracked and exposed via view helpers (e.g.,
getCollectionBalance,getBasePrice,getCollections,getActiveCollections).
- Owner configures collections and their weights via
-
Automated buying & price caps:
- Anyone can call
buyNFTForCollection(...)to purchase a floor NFT for a configured collection using that collection’s ETH balance. - DutchVault enforces:
- Sufficient allocated balance and non‑zero base price.
- Function selector allowlisting via
setAllowedMarketplaceSelector(marketplace, selector, true)for the purchase function used when buying NFTs. - A dynamic max price cap per collection (
getMaxPriceForCollection) based onbasePrice + blocksSinceLastBuy × buyIncrement.
- Successful purchases create on‑chain inventory records (
InventoryRecord) and update metrics like_totalNFTsPurchasedand_totalCostETH.
- Anyone can call
-
Listing & settlement flow:
- Anyone can:
listNFTOnMarketplace(...)for held NFTs, which computes min/max auction prices from configurable multipliers (setAuctionMultipliers).buyAndListNFT(...)in a single transaction.
- Listings are created on
DutchAuctionMarketplaceeither as:- Standard listings (proceeds returned to the vault), or
- Burn listings (all DUTCH proceeds burned) when there are no AuctionAssist contributions.
- Settlements can be:
- Recorded manually via
recordSettlement(...)(owner‑only), or - Automatically via
onListingSettled(...)callback from the marketplace.
- Recorded manually via
- Settlement logic (
_processSettlement) updates:- Inventory flags (listed/realized, items held/sold),
- Total DUTCH burned (
getTotalDUTCHBurned), - Realized profit/loss (
getTotalRealizedProfit,getTotalRealizedLoss), - And then calls
_splitProceeds(...)to share DUTCH between AuctionAssist contributors and protocol burns.
- Anyone can:
-
AuctionAssist integration:
setAuctionAssist(address)wires in the AuctionAssist contract (or disables it withaddress(0)).receiveContribution(...)lets AuctionAssist push ETH into a specific collection balance and updates_totalETHReceived.- On purchases, if AuctionAssist is configured, DutchVault notifies it via
IAuctionAssist.recordPurchase(...), and on settlement it shares DUTCH proceeds pro‑rata with contributors while burning the protocol’s share viaDUTCHToken.userBurn.
-
Admin & safety:
- Owner can pause/unpause all external flows (
pause,unpause) and withdraw stranded per‑collection ETH viawithdrawCollectionBalance(...)(with balance and zero‑withdrawal safeguards). - All NFT‑moving and settlement paths are
nonReentrant, and marketplace interactions are guarded with explicit checks and custom errors (e.g.,MarketplaceCallFailed,NFTNotAcquired,NFTAlreadyOwned).
- Owner can pause/unpause all external flows (
Marketplace infrastructure for NFT listings and planned deflationary auction mechanisms.
Component Summary:
- Dutch auctions with configurable max/min prices and durations.
- Protocol listings with burn to route proceeds directly into DUTCH burns when there are no external contributors.
- Configurable listing fees and whitelisting to control which collections can trade and how fees are split.
Core Functions:
| Component | Function |
|---|---|
| Listing Creation | createListing() - Users list NFTs with max/min ETH prices |
| Protocol Listings | createListingWithBurn() - Owner creates listings with burn recipient |
| Listing Management | cancelListing() - Seller or current owner can cancel |
| Settlement | settle() - Purchase with DUTCH tokens, or settleWithETH() - Purchase with ETH (protocol listings only) |
| Collection Whitelist | Optional filtering of allowed NFT collections |
| Listing Fee (createListing) | 0.5% of min price (min 0.1 ETH), split 75/25 to NPA/Ops (default, configurable) |
Fee Structure:
- Listing Fee:
max(minPrice × 0.5%, 0.1 ETH)paid in ETH when creating listing - Listing Fee Distribution: 75% to Dutch Vault (NPA), 25% to Protocol Operations (default, configurable)
- Settlement Fee: 2.5% of sale price (configurable)
- Settlement Fee Distribution: 100% burned for third-party listings (default, configurable split between Vault/Ops/Burn)
- Auction Duration: 12 hours (configurable default)
Configuration Options:
| Setting | Function | Parameters | Limits | Default |
|---|---|---|---|---|
| Collection Whitelist | setCollectionWhitelistEnabled(bool) |
true/false |
- | false |
| Collection Access | setCollectionAllowed(address, bool) |
Collection address, allowed | - | All allowed (if whitelist disabled) |
| Emergency Pause | pause()/unpause() |
- | - | Unpaused |
Note: Settlement fees (2.5%), listing fees (0.5%), listing fee minimum (0.1 ETH), auction duration (12 hours), and all fee split ratios are configurable.
The auction assist system lets users deposit ETH to top up DutchVault’s per‑collection balances and receive pro‑rata DUTCH rewards when those NFTs are eventually sold.
Component Summary:
-
ETH in → DUTCH out: Users contribute ETH toward specific collections and later receive a share of DUTCH proceeds when NFTs sell.
-
Bounded contributor sets per collection to keep gas predictable.
-
Automatic burning of any non‑allocated DUTCH, reinforcing deflationary tokenomics.
-
Contribution flow:
- Users call
contribute(collection)with ETH. - AuctionAssist:
- Validates the collection has a non‑zero base price in DutchVault (
getBasePrice), otherwise revertsCollectionNotConfigured. - Computes a contribution percentage in BPS relative to that base price and records it in a per‑user
ContributionRecord(amount,percentageBPS). - Tracks contributors per collection (capped at
MAX_CONTRIBUTORS = 100to keep gas bounded) and which collections each user has positions in. - Forwards the ETH directly to DutchVault via
receiveContribution(collection, contributor).
- Validates the collection has a non‑zero base price in DutchVault (
- Users call
-
Purchase snapshotting (DutchVault → AuctionAssist):
- When DutchVault buys an NFT for a collection, it calls
recordPurchase(collection, tokenId, costETH):- Only DutchVault is authorized (
UnauthorizedCallerotherwise). - A new
purchaseIdis created, and anNFTPurchaseInfois stored (collection, tokenId, cost, total contributed ETH, list of contributors). - For each contributor on that collection, AuctionAssist snapshots their share in BPS (
_purchaseShares[purchaseId][contributor]) relative to the actual cost (capped at 10000 BPS). - Emits
PurchaseRecorded(purchaseId, collection, tokenId, costETH, contributorCount).
- Only DutchVault is authorized (
- When DutchVault buys an NFT for a collection, it calls
-
Reward distribution & burns:
- On settlement, DutchVault calls
recordAndPullRewards(purchaseId, dutchAmount)on AuctionAssist (wheredutchAmountis the DUTCH proceeds share for that purchase). AuctionAssist validates the purchase, marks rewards as funded, and pulls DUTCH from DutchVault; the protocol share is later burned viaburnProtocolShare(purchaseId)once all contributors have claimed. - Contributors call
claimReward(purchaseId)to receive their pro‑rata DUTCH. Each claim is limited to the contributor's share;RewardsAlreadyClaimedprevents double claims. Unclaimed protocol share is burned viaburnProtocolShare(purchaseId)(callable by anyone once all contributors have claimed).
- On settlement, DutchVault calls
-
Views & UX helpers:
getContribution(user, collection)– raw contribution record (amount + percentageBPS).getTotalContributions(collection)– total ETH contributed for a collection.getPurchaseInfo(purchaseId)/getPurchaseShare(purchaseId, contributor)– inspection of per‑purchase snapshots.getCollectionContributors(collection)– list of contributors for a collection.getUserCollections(user)andgetUserActivePositions(user)– front‑end friendly views of all positions a user has across collections.
-
Admin & safety:
- Owner can
pause/unpauseall contributions and reward distributions. - Uses
ReentrancyGuardandSafeERC20for safe external calls and token transfers. - Strong input validation and custom errors prevent misconfiguration (e.g., zero addresses, zero contribution amounts, invalid purchase IDs).
- Owner can
Optional presale module that collects ETH during a fixed window, converts it to DUTCH via the bonding‑curve engine, and creates Sablier V2 vesting streams for early participants.
Component Summary:
-
Time‑boxed ETH raise with a hard cap and minimum ticket size.
-
Single atomic finalization that mints via the curve and sets up all vesting streams.
-
On‑chain vesting analytics and optional claim helper on top of Sablier’s own UI.
-
Configuration & lifecycle:
- Immutable parameters set at deployment:
presaleStart– timestamp when deposits open.presaleCap– maximum ETH that can be raised.PRESALE_DURATION– duration of the sale (7 days).VESTING_CLIFF/VESTING_DURATION– 6‑month cliff + 12‑month linear vest.
- Owner must later wire dependencies via:
setContracts(dutchToken, bondingCurve)– connectsIDUTCHTokenandBondingCurve.setSablierV2Address(address)– sets the Sablier Lockup Linear contract (before vesting starts).
- Immutable parameters set at deployment:
-
Deposit phase:
- Users call
depositEth():- Enforces min contribution (
MIN_CONTRIBUTION), time window (presaleStart→presaleStart + PRESALE_DURATION), and global cap (presaleCap). - Tracks per‑user contributions and maintains a
contributorsarray. - Emits
EthDeposited(user, amount).
- Enforces min contribution (
- View helpers:
checkFundedAmount()– totaltotalFundedAmount.getUserContribution(user)– each user’s ETH.getContributorCount()– number of unique contributors.
- Users call
-
Upkeep & finalization:
checkUpkeep(...)(Chainlink‑style) reports when the sale is ready to finalize:- Requires token, BondingCurve, and Sablier addresses to be set.
- Presale must be ended,
totalFundedAmount > 0, andvestingInitiated == false.
performUpkeep(...)can be called by anyone oncecheckUpkeepis true:- Internally:
- Calls
_mintTokens()which sends all ETH toBondingCurve.buyDutchExactIn()(BondingCurve handles tax distribution internally). - Stores minted token amounts and approves Sablier for stream creation.
- Marks
vestingInitiated = trueand setsvestingStartTime. - Emits
PresaleFinalized(totalEth, totalTokens, contributorCount).
- Calls
- After
performUpkeep(), individual contributors must callclaimVestingStream(user)to create their vesting stream.
- Internally:
finalizePresale()is a convenience wrapper aroundperformUpkeep().
-
Bonding‑curve quote helpers:
getPresaleQuote(ethAmount)– view helper that:- Before vesting: calls
bondingCurve.getQuoteWETHtoDUTCH(ethAmount)to get expected tokens, tax, and net ETH. - After vesting: returns stored values from
totalVestingTokensDetails(dutchAmount, taxAmount, ethUsed).
- Before vesting: calls
getUserAllocation(user)– returns expected token allocation for a user’s ETH contribution given the finaltotalFundedAmount.
-
Sablier V2 vesting:
_createVestingStreams():- Reads the full DUTCH balance of the presale contract after minting and approves Sablier.
- For each contributor:
- Computes
userTokenAllocation = totalTokens * userContribution / totalFundedAmount. - Builds
LockupLineardurations and unlock amounts (0 at start, 100% at cliff). - Calls
sablierV2.createWithDurationsLL(...)and stores thestreamIdinvestingStreamIds[user]. - On success, emits
TokensClaimed(user, allocation)as a “stream created with this amount” signal. - On failure, emits
StreamCreationFailed(user, allocation)but continues with other contributors.
- Computes
- Emits
VestingStreamsCreated(participantCount)with the number of successful streams.
-
User vesting views & claiming:
getUserVestingInfo(user)– returns stream ID, total receivable, unlocked, claimed, and full schedule (start, cliff, end), plus a progress BPS (0–10000).canUserClaim(user)– helper that checks cliff time and returns(canClaim, timeUntilCliff).getUserStreamId(user)– direct access tovestingStreamIds[user]for off‑chain Sablier interactions.claimVestedTokens(amount)– optional convenience:- Uses Sablier’s
withdrawMaxwhenamount == 0orwithdrawfor a specified amount. - Emits
TokensClaimed(user, withdrawnAmount)for app‑level tracking.
- Uses Sablier’s
- Foundry toolkit
- Solidity 0.8.26
└── src/
├── DUTCHToken.sol
├── BondingCurve.sol
├── DutchAuctionMarketplace.sol
├── DutchVault.sol
├── AuctionAssist.sol
├── Presale.sol
└── interfaces/
├── IDUTCHToken.sol
├── IDutchVault.sol
├── IDutchAuctionMarketplace.sol
└── IAuctionAssist.sol
| Contract | Inherits From | Interfaces Used | Math Libraries |
|---|---|---|---|
| DUTCHToken | ERC20, Ownable2Step, Pausable |
- | - |
| BondingCurve | Ownable2Step, ReentrancyGuard, Pausable |
IDUTCHToken, IWETH9 |
UD60x18 (PRB Math) |
| DutchAuctionMarketplace | Ownable2Step, Pausable, ReentrancyGuard |
IDUTCHToken, IDutchVault, IDutchAuctionMarketplace, IERC721 |
SafeERC20 |
| DutchVault | Ownable2Step, Pausable, ReentrancyGuard, IERC721Receiver |
IDUTCHToken, IDutchAuctionMarketplace, IAuctionAssist, IERC721 |
SafeERC20 |
| AuctionAssist | Ownable2Step, Pausable, ReentrancyGuard |
IDutchVault, IDUTCHToken, IAuctionAssist |
SafeERC20 |
| Presale | Ownable2Step, ReentrancyGuard, Pausable |
IDUTCHToken, BondingCurve, ISablierLockup |
UD60x18 (PRB Math) |
| IDUTCHToken | Interface | - | - |
| Library | Purpose | Used In |
|---|---|---|
| UD60x18 (PRB Math) | Fixed-point arithmetic for bonding curve calculations | BondingCurve.sol, Presale.sol |
| SafeERC20 (OpenZeppelin) | Safe ERC-20 transfers and allowance management | DutchAuctionMarketplace.sol, DutchVault.sol, AuctionAssist.sol |
| Interface | Purpose | Used In |
|---|---|---|
| IERC721 (OpenZeppelin) | NFT interface for marketplace operations | DutchAuctionMarketplace.sol, DutchVault.sol |
| IDUTCHToken (Custom) | Token interface for bonding curve and protocol interactions | DUTCHToken.sol, BondingCurve.sol, DutchAuctionMarketplace.sol, DutchVault.sol, AuctionAssist.sol, Presale.sol |
| IDutchVault (Custom) | Interface for the DutchVault treasury | DutchVault.sol, DutchAuctionMarketplace.sol, AuctionAssist.sol |
| IDutchAuctionMarketplace (Custom) | Typed access to the Dutch auction marketplace | DutchAuctionMarketplace.sol, DutchVault.sol |
| IAuctionAssist (Custom) | Interface for the Auction Assist module | DutchVault.sol, AuctionAssist.sol |
| ISablierLockup (Sablier V2) | Token streaming and lockup primitives for presale vesting | Presale.sol |
| IWETH9 (Uniswap v4-periphery) | Wrapped ETH interface used by BondingCurve | BondingCurve.sol |
Dependencies are managed with Soldeer for secure, versioned package management.
| Name | Version | Commit/Tag | GitHub |
|---|---|---|---|
| OpenZeppelin Contracts | v5.5.0 | fcbae53 | https://github.com/OpenZeppelin/openzeppelin-contracts |
| PRB Math | v4.1.0 | 280fc5f | https://github.com/PaulRBerg/prb-math |
| Forge Standard Library | v1.14.0 | - | https://github.com/foundry-rs/forge-std |
| Solmate | v1.0.0 | 89365b8 | https://github.com/transmissions11/solmate |
| Uniswap v4 Periphery | v1.0.3 | 3779387 | https://github.com/Uniswap/v4-periphery |
| Sablier Lockup | v2.0.1 | baf9a9e | https://github.com/sablier-labs/lockup |
- Clone the repository:
git clone [repository-url]
cd Protocol-Contracts- Install Foundry (if not already installed):
curl -L https://foundry.paradigm.xyz | bash
foundryup- Install dependencies:
See BUILD_GUIDE.md for detailed build instructions and dependency management with Soldeer.
- Build the project:
forge buildRun the test suite:
forge testThis project is licensed under the BSL-1.1 (Business Source License 1.1).
