DEV Community

rayQu
rayQu

Posted on

Building a Confidential Sealed-Bid NFT Auction on Oasis Sapphire

Ever wanted to run a blockchain auction without revealing bids until the end?

In this tutorial, we’ll build a sealed-bid NFT auction on Oasis Sapphire, where bids are fully encrypted, events are private, and the winner is publicly verifiable, perfect for NFT drops, private DeFi auctions, or confidential governance.

We’ll cover:

  • Why Sapphire for privacy-first auctions
  • Smart contract design for sealed-bid auctions
  • Off-chain bid commitment and encryption
  • Encrypted events on Sapphire
  • React + ethers.js frontend integration
  • Testing, deployment, and advanced tips

By the end, you’ll have a production-ready privacy-preserving auction.


Part 1: Why Privacy Matters on Blockchain Auctions

Traditional Ethereum-style auctions expose bids as soon as they hit the blockchain. This causes issues like:

  • Front-running attacks: Bots see your bid and immediately outbid you
  • Loss of privacy: High-value bids are visible to competitors
  • Manipulation risk: Public bids allow strategic manipulation

Oasis Sapphire solves this by offering:

  • Confidential calldata: Input values are encrypted
  • Encrypted events: Logs can be private but verifiable
  • EVM compatibility: Solidity contracts work with minimal changes

So the result: You can build auctions where nobody knows your bid until reveal, yet the outcome is fully trustless.

┌─────────────┐        submitBid(commitment)       ┌───────────────┐
│   Bidder A  │ ────────────────────────────────>  │  SealedAuction│
└─────────────┘                                    │ (encrypted)   │
┌─────────────┐        submitBid(commitment)       │               │
│   Bidder B  │ ────────────────────────────────>  │               │
└─────────────┘                                    └─────┬─────────┘
                                                      │
                                                      ▼
                                        ┌───────────────────────┐
                                        │ Reveal Phase          │
                                        │ - submit value+salt   │
                                        │ - verify commitment   │
                                        │ - determine winner    │
                                        └─────────┬─────────────┘
                                                  │
                                                  ▼
                                       ┌─────────────────────────┐
                                       │ Winner + Final Price    │
                                       │ (publicly verifiable)   │
                                       └─────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Part 2: Project Setup

  1. Create Hardhat Project
mkdir sapphire-nft-auction
cd sapphire-nft-auction
npm init -y
npm install --save-dev hardhat @nomiclabs/hardhat-ethers ethereum-waffle chai
npm install @oasisprotocol/sapphire-paratime
Enter fullscreen mode Exit fullscreen mode
  1. Configure Hardhat for Sapphire Testnet
require("@nomiclabs/hardhat-ethers");
require("@oasisprotocol/sapphire-paratime");

module.exports = {
  solidity: "0.8.20",
  networks: {
    sapphire_testnet: {
      url: "https://testnet.sapphire.oasis.io",
      chainId: 23295,
      accounts: [process.env.PRIVATE_KEY], // your testnet wallet
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Make sure you fund your testnet wallet with some ROSE test tokens (Testnet Faucet)


Part 3: Smart Contract Architecture

We’ll implement a sealed-bid auction:

  • submitBid: Accepts encrypted bid commitments
  • reveal: Checks commitments, reveals bids, determines winner
  • events: Logs encrypted bid submissions

Create contracts/SealedAuction.sol:

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

contract SealedAuction {
    struct Bid {
        bytes32 commitment;
        bool revealed;
    }

    address public owner;
    address public winner;
    uint256 public finalPrice;

    mapping(address => Bid) public bids;

    uint256 public biddingEnd;
    uint256 public revealEnd;

    event BidEncrypted(address indexed bidder, bytes32 encrypted);
    event Revealed(address indexed bidder, uint256 value);

    constructor(uint256 _biddingTime, uint256 _revealTime) {
        owner = msg.sender;
        biddingEnd = block.timestamp + _biddingTime;
        revealEnd  = biddingEnd + _revealTime;
    }

    function submitBid(bytes32 commitment) external {
        require(block.timestamp < biddingEnd, "Bidding closed");
        bids[msg.sender] = Bid({commitment: commitment, revealed: false});
        emit BidEncrypted(msg.sender, commitment);
    }

    function reveal(uint256 value, bytes32 salt) external {
        require(block.timestamp >= biddingEnd, "Reveal not started");
        require(block.timestamp < revealEnd, "Reveal ended");

        Bid storage b = bids[msg.sender];
        require(b.commitment == keccak256(abi.encodePacked(value, salt)), "Bad reveal");
        require(!b.revealed, "Already revealed");

        b.revealed = true;
        emit Revealed(msg.sender, value);

        if (value > finalPrice) {
            finalPrice = value;
            winner = msg.sender;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Key points to keep in mind:

  • Only the hash of the bid is stored on-chain, not the bid itself.
  • Bids remain private until reveal().
  • Events can optionally be encrypted for extra confidentiality.

Part 4: Off-Chain Bid Commitment

Before submitting a bid, you generate a commitment hash off-chain:

import { ethers } from "ethers";

function createCommitment(bidValue) {
  const salt = ethers.utils.hexlify(ethers.utils.randomBytes(32));
  const commitment = ethers.utils.keccak256(
    ethers.utils.defaultAbiCoder.encode(["uint256","bytes32"], [bidValue, salt])
  );
  return { commitment, salt };
}

// Example
const bid = createCommitment(500);
console.log("Commitment:", bid.commitment, "Salt:", bid.salt);
Enter fullscreen mode Exit fullscreen mode
  • Salt prevents brute-force attacks on commitments
  • The commitment hash is sent on-chain using submitBid()
Bidder Client
+-----------------+
| bidValue + salt |
+--------+--------+
         |
         | keccak256 -> commitment
         v
+-----------------+     encrypted calldata    +------------------------+
| submitBid()     | ------------------------> | SealedAuction Contract |
| (commitment)    |                           | stores only commitment |
+-----------------+                           |                        |
                                              +-----------+------------+
                                                          |
                                               encrypted event emitted
                                                          |
                                                          v
                                               Off-chain listener/frontend
                                               (decrypts if authorized)
Enter fullscreen mode Exit fullscreen mode

Part 5: Submitting & Revealing Bids

await auctionContract.submitBid(commitment);

// Wait until bidding period ends
await auctionContract.reveal(bidValue, salt);
Enter fullscreen mode Exit fullscreen mode

On Sapphire, you can use encrypted calldata so even miners/validators don’t see the bid during submission.


Part 6: Adding NFT Rewards

Let’s take it a step further: the winner receives an NFT. Add this to your contract:

interface IERC721 {
    function safeMint(address to) external;
}

IERC721 public rewardNFT;

constructor(address _nft) {
    owner = msg.sender;
    rewardNFT = IERC721(_nft);
}

// After reveal ends
function distributeNFT() external {
    require(block.timestamp >= revealEnd, "Reveal not ended");
    require(winner != address(0), "No winner yet");
    rewardNFT.safeMint(winner);
}
Enter fullscreen mode Exit fullscreen mode

Part 7: Encrypted Events on Sapphire (Advanced)

event EncryptedBid(address indexed bidder, bytes32 encryptedData);

bytes memory ciphertext = Sapphire.encrypt(abi.encodePacked(bidValue, salt));
emit EncryptedBid(msg.sender, ciphertext);
Enter fullscreen mode Exit fullscreen mode
  • Only the bidder can decrypt the event off-chain
  • Useful for building off-chain bid monitoring tools without revealing bids publicly

Part 8: Frontend Integration (React + ethers.js)

  • Generate bid commitment in React form
  • Call submitBid(commitment) on click
  • Reveal after auction ends
  • Optional: use encrypted events to show notifications without revealing bid amounts
const bid = createCommitment(inputValue);
await auctionContract.submitBid(bid.commitment);
Enter fullscreen mode Exit fullscreen mode
  • Add a timer for bidding and reveal periods
  • Use ethers.js and MetaMask for transactions

Part 9: Hardhat Testing

const { expect } = require("chai");

describe("SealedAuction", function () {
  it("accepts bids and picks winner correctly", async function () {
    const [a, b] = await ethers.getSigners();
    const Auction = await ethers.getContractFactory("SealedAuction");
    const auction = await Auction.deploy(60, 120);

    const bidA = ethers.utils.id("1001salt");
    const bidB = ethers.utils.id("2002salt2");

    await auction.submitBid(bidA);
    await auction.submitBid(bidB);

    await network.provider.send("evm_increaseTime", [70]);

    await auction.reveal(1001, ethers.utils.formatBytes32String("salt"));
    await auction.reveal(2002, ethers.utils.formatBytes32String("salt2"));

    expect(await auction.winner()).to.equal(b.address);
    expect(await auction.finalPrice()).to.equal(2002);
  });
});
Enter fullscreen mode Exit fullscreen mode

Part 10: Deploying to Sapphire Testnet

npx hardhat run scripts/deploy.js --network sapphire_testnet

Enter fullscreen mode Exit fullscreen mode

Deploy script:

async function main() {
  const Auction = await ethers.getContractFactory("SealedAuction");
  const auction = await Auction.deploy(300, 300);
  await auction.deployed();
  console.log("Auction deployed at", auction.address);
}
main();
Enter fullscreen mode Exit fullscreen mode

Part 11: Next-Level Ideas you could implement

  • Build a full frontend auction dApp
  • Integrate off-chain listeners for encrypted events
  • Add ZK proofs for bid fairness without revealing bids
  • Support multi-item auctions or batch NFT rewards

You now have a fully functional, privacy-preserving NFT auction on Oasis Sapphire. Bids are private, events can be private, and the winner is verifiable. Perfect for real-world confidential DeFi and NFT applications!

Top comments (0)