DEV Community

rayQu
rayQu

Posted on

Build a Private Onchain Feedback Box with Oasis Sapphire

What if you wanted to collect honest feedback onchain without publishing everyone's answers?

A normal Ethereum smart contract is a strange place for something like this.

You can store feedback cheaply and transparently, but anyone can inspect the contract's storage or events. That is useful for many applications, but not for anonymous feedback, internal surveys, product reviews, private ratings, or user research.

This is where Oasis Sapphire becomes interesting.

Sapphire is an EVM-compatible confidential environment where contract state is encrypted and confidential computation happens inside Oasis infrastructure. You can therefore keep sensitive application data private while still using familiar Solidity and EVM tooling.

In this tutorial, we'll build a small private feedback box.

Users will be able to submit:

  • a 1–5 rating
  • a text comment

The individual comments will remain confidential.

The contract will expose only aggregate information such as:

  • number of submissions
  • total rating
  • average rating

So the application can answer:

“What do users think?”

without automatically publishing:

“What did Alice specifically say?”


What We're Building

The architecture is intentionally small.

                    ┌──────────────────────┐
                    │      User Wallet     │
                    └──────────┬───────────┘
                               │
                       Encrypted TX
                               │
                               ▼
                    ┌──────────────────────┐
                    │   Oasis Sapphire     │
                    │                      │
                    │  Private feedback    │
                    │  + aggregate stats   │
                    └──────────┬───────────┘
                               │
                 ┌─────────────┴─────────────┐
                 │                           │
                 ▼                           ▼
          Private comments            Public aggregates
          + ratings                   count / total
Enter fullscreen mode Exit fullscreen mode

The important design decision is that we don't emit the feedback itself as a normal Solidity event.

That's important because standard EVM events are public on Sapphire too. If you put sensitive feedback directly into an event, you can undermine the confidentiality of the underlying state. Oasis provides encrypted events for applications that specifically need confidential event payloads.

For our simple application, we'll just keep the feedback in contract state.


1. Create the Project

We'll use Hardhat and TypeScript.

Create a project:

mkdir private-feedback-box
cd private-feedback-box

npm init -y

npm install --save-dev \
  hardhat \
  @nomicfoundation/hardhat-toolbox \
  typescript \
  ts-node

npm install \
  @oasisprotocol/sapphire-contracts \
  @oasisprotocol/sapphire-paratime
Enter fullscreen mode Exit fullscreen mode

Oasis Sapphire is EVM-compatible, so existing Ethereum development tools such as Hardhat can be used. The main addition is Oasis' Sapphire tooling for confidential transactions and contract functionality.

Your project can look like this:

private-feedback-box/
│
├── contracts/
│   └── FeedbackBox.sol
│
├── scripts/
│   └── deploy.ts
│
├── test/
│   └── FeedbackBox.ts
│
├── hardhat.config.ts
├── package.json
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

2. Configure Sapphire

Create hardhat.config.ts:

import { HardhatUserConfig } from "hardhat/config";
import "@oasisprotocol/sapphire-hardhat";
import "@nomicfoundation/hardhat-toolbox";

const accounts = process.env.PRIVATE_KEY
  ? [process.env.PRIVATE_KEY]
  : {
      mnemonic:
        "test test test test test test test test test test test junk",
      path: "m/44'/60'/0'/0",
      initialIndex: 0,
      count: 20,
      passphrase: "",
    };

const config: HardhatUserConfig = {
  solidity: {
    version: "0.8.24",
    settings: {
      evmVersion: "paris",
    },
  },

  networks: {
    sapphire: {
      url: "https://sapphire.oasis.io",
      chainId: 0x5afe,
      accounts,
    },

    "sapphire-testnet": {
      url: "https://testnet.sapphire.oasis.io",
      chainId: 0x5aff,
      accounts,
    },

    "sapphire-localnet": {
      url: "http://localhost:8545",
      chainId: 0x5afd,
      accounts,
    },
  },
};

export default config;
Enter fullscreen mode Exit fullscreen mode

The official Sapphire quickstart currently uses these Sapphire network identifiers and RPC endpoints for mainnet, testnet, and localnet.

The important part is:

import "@oasisprotocol/sapphire-hardhat";
Enter fullscreen mode Exit fullscreen mode

This allows the Hardhat environment to apply Sapphire's transaction handling automatically.


3. Write the Feedback Contract

Create:

contracts/FeedbackBox.sol
Enter fullscreen mode Exit fullscreen mode

Then add:

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

contract FeedbackBox {
    struct Feedback {
        uint8 rating;
        string comment;
        uint256 timestamp;
    }

    mapping(address => Feedback) private feedback;

    uint256 private submissionCount;
    uint256 private totalRating;

    event FeedbackSubmitted(address indexed user);

    function submitFeedback(
        uint8 rating,
        string calldata comment
    ) external {
        require(rating >= 1 && rating <= 5, "rating must be 1-5");
        require(bytes(comment).length > 0, "comment required");

        require(
            feedback[msg.sender].timestamp == 0,
            "feedback already submitted"
        );

        feedback[msg.sender] = Feedback({
            rating: rating,
            comment: comment,
            timestamp: block.timestamp
        });

        submissionCount++;
        totalRating += rating;

        emit FeedbackSubmitted(msg.sender);
    }

    function getMyFeedback()
        external
        view
        returns (
            uint8 rating,
            string memory comment,
            uint256 timestamp
        )
    {
        Feedback memory item = feedback[msg.sender];

        require(item.timestamp != 0, "no feedback");

        return (
            item.rating,
            item.comment,
            item.timestamp
        );
    }

    function getStats()
        external
        view
        returns (
            uint256 count,
            uint256 total,
            uint256 average
        )
    {
        count = submissionCount;
        total = totalRating;

        if (count > 0) {
            average = total / count;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

At first glance, this looks almost identical to a normal Solidity contract.

That's intentional.

The interesting part is not a new programming language.

It's the execution environment.


4. Why private Matters Differently Here

On Ethereum, developers sometimes misunderstand Solidity's private keyword.

This:

mapping(address => Feedback) private feedback;
Enter fullscreen mode Exit fullscreen mode

does not make the data cryptographically private on Ethereum.

It only prevents other Solidity contracts from directly accessing the variable.

Blockchain observers can still inspect Ethereum's underlying state.

Sapphire changes the model.

Sapphire's contract state is encrypted, and the contract running inside the confidential environment is the entity that can request the relevant state encryption key.

That means our feedback mapping can actually contain confidential application data.

So this:

mapping(address => Feedback) private feedback;
Enter fullscreen mode Exit fullscreen mode

is meaningful for confidentiality when deployed on Sapphire.

But there is an important detail.


5. Don't Accidentally Publish the Feedback

This event is okay:

event FeedbackSubmitted(address indexed user);
Enter fullscreen mode Exit fullscreen mode

It tells observers that an address submitted feedback.

But this would be a bad idea:

event FeedbackSubmitted(
    address indexed user,
    uint8 rating,
    string comment
);
Enter fullscreen mode Exit fullscreen mode

Why?

Because normal event payloads are public.

Sapphire's documentation explicitly notes that Solidity events behave like Ethereum events, meaning event data is not automatically confidential.

If you need confidential event data, Sapphire supports encrypted event payloads.

For our beginner example, however, we simply don't put the sensitive information into the event.


6. Add a Simple Deployment Script

Create:

scripts/deploy.ts
Enter fullscreen mode Exit fullscreen mode
import { ethers } from "hardhat";

async function main() {
  const FeedbackBox =
    await ethers.getContractFactory("FeedbackBox");

  const feedbackBox =
    await FeedbackBox.deploy();

  await feedbackBox.waitForDeployment();

  console.log(
    "FeedbackBox deployed to:",
    await feedbackBox.getAddress()
  );
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
Enter fullscreen mode Exit fullscreen mode

Compile:

npx hardhat compile
Enter fullscreen mode Exit fullscreen mode

Then deploy to Sapphire Testnet:

npx hardhat run scripts/deploy.ts \
  --network sapphire-testnet
Enter fullscreen mode Exit fullscreen mode

Before deploying to testnet, you'll need Sapphire testnet tokens from the Oasis faucet.

For rapid local development, Oasis also provides a Sapphire Localnet that runs in Docker. The official documentation recommends Localnet or Sapphire Testnet when testing confidential features.


7. Submit Private Feedback

You can interact with the contract from a Hardhat task, script, or frontend.

A simple example:

const feedbackBox =
  await ethers.getContractAt(
    "FeedbackBox",
    CONTRACT_ADDRESS
  );

const tx = await feedbackBox.submitFeedback(
  5,
  "The application is fast and easy to use."
);

await tx.wait();

console.log("Feedback submitted");
Enter fullscreen mode Exit fullscreen mode

The important part is that the transaction containing the comment should be sent through Sapphire's confidential transaction tooling.

For browser applications, Oasis provides wrappers for common EVM libraries including Ethers, Viem, and Wagmi.

For example, the Sapphire TypeScript wrapper can wrap an injected wallet provider:

import {
  wrapEthereumProvider
} from "@oasisprotocol/sapphire-paratime";

const provider =
  wrapEthereumProvider(window.ethereum);
Enter fullscreen mode Exit fullscreen mode

This is the layer that makes it practical to use normal Web3 wallets while still sending confidential transactions.


8. Reading the Statistics

Now we have an interesting separation.

Anyone can call:

getStats()
Enter fullscreen mode Exit fullscreen mode

and retrieve:

Submissions: 143
Total rating: 587
Average: 4
Enter fullscreen mode Exit fullscreen mode

But that doesn't reveal every individual comment.

Conceptually:

                   Feedback
                       │
             ┌─────────┴─────────┐
             │                   │
             ▼                   ▼
       Private state         Aggregates
             │                   │
             │                   └── count
             │                   └── total
             │                   └── average
             │
             └── rating
             └── comment
             └── timestamp
Enter fullscreen mode Exit fullscreen mode

This is a useful pattern far beyond feedback.

The same architecture can work for:

  • private product reviews
  • internal company surveys
  • anonymous community feedback
  • private ratings
  • confidential research
  • user preference storage
  • private application configuration

The general pattern is:

Store sensitive information privately. Expose only the derived information that needs to be public.


9. What About getMyFeedback()?

There's one subtle Sapphire concept worth understanding.

This function:

function getMyFeedback()
    external
    view
    returns (...)
{
    Feedback memory item = feedback[msg.sender];

    require(item.timestamp != 0, "no feedback");

    return (...);
}
Enter fullscreen mode Exit fullscreen mode

works naturally for authenticated calls.

But a plain eth_call on Sapphire does not automatically give the contract an arbitrary authenticated msg.sender. For unauthenticated view calls, Sapphire uses the zero address.

This is important because confidential data requires stronger access control than a traditional public EVM application.

For frontend applications, Oasis documents SIWE-based authenticated view calls.

For backend services, Sapphire also supports signed queries using EIP-712.

So a production version of this feedback box would typically use authenticated view calls when users need to retrieve their private feedback.


10. A Minimal Test

Create:

test/FeedbackBox.ts
Enter fullscreen mode Exit fullscreen mode
import { expect } from "chai";
import { ethers } from "hardhat";

describe("FeedbackBox", function () {
  it("stores feedback and updates statistics", async function () {
    const [user] = await ethers.getSigners();

    const FeedbackBox =
      await ethers.getContractFactory("FeedbackBox");

    const box =
      await FeedbackBox.deploy();

    await box.waitForDeployment();

    await box.submitFeedback(
      5,
      "Excellent experience"
    );

    const stats = await box.getStats();

    expect(stats.count).to.equal(1);
    expect(stats.total).to.equal(5);
    expect(stats.average).to.equal(5);
  });

  it("prevents duplicate submissions", async function () {
    const FeedbackBox =
      await ethers.getContractFactory("FeedbackBox");

    const box =
      await FeedbackBox.deploy();

    await box.waitForDeployment();

    await box.submitFeedback(
      4,
      "Good experience"
    );

    await expect(
      box.submitFeedback(
        5,
        "Trying again"
      )
    ).to.be.revertedWith(
      "feedback already submitted"
    );
  });
});
Enter fullscreen mode Exit fullscreen mode

Run:

npx hardhat test
Enter fullscreen mode Exit fullscreen mode

For testing Sapphire-specific confidentiality, use Sapphire Localnet or Sapphire Testnet rather than relying only on a standard Ethereum-style local chain. Oasis specifically recommends a Sapphire-supported network for testing confidential features.


11. One Important Production Improvement

Our example intentionally keeps the contract simple.

A production application should think carefully about metadata leakage.

Confidentiality does not necessarily hide everything about execution.

Oasis documents several potential side channels, including gas usage, execution time, storage-access patterns, and transaction size. The size of encrypted state is also not automatically hidden.

For example, this:

if (bytes(comment).length > 1000) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

could potentially create observable differences in execution.

Likewise, variable-length sensitive inputs can expose their size.

For higher-security applications, consider:

  • fixed-size inputs
  • padding
  • bucketized sizes
  • constant-ish execution paths
  • avoiding sensitive values in normal events
  • carefully designed access control

These considerations become increasingly important as an application moves from a demo to production.


12. Why This Pattern Is Interesting

The feedback box looks almost trivial.

That's actually the point.

A blockchain doesn't have to put every piece of application data into public state.

Instead, you can divide application information into two categories:

Public information

Things users need to verify:

How many submissions?
What is the average rating?
Has this address submitted?
Enter fullscreen mode Exit fullscreen mode

Private information

Things the application needs but the entire network doesn't:

What did the user write?
What rating did they give?
What private metadata is attached?
Enter fullscreen mode Exit fullscreen mode

Sapphire gives developers an environment where that distinction can exist inside an EVM-compatible application.

You don't have to abandon Solidity.

You don't need to invent a new smart-contract language.

You simply design the application around confidential state from the beginning.


Final Architecture

The finished application is conceptually simple:

User
 │
 │ encrypted transaction
 ▼
┌──────────────────────────────┐
│       Oasis Sapphire         │
│                              │
│  ┌────────────────────────┐  │
│  │ Confidential Feedback  │  │
│  │                        │  │
│  │ rating                 │  │
│  │ comment                │  │
│  │ timestamp              │  │
│  └────────────────────────┘  │
│                              │
│  ┌────────────────────────┐  │
│  │ Public Aggregates      │  │
│  │                        │  │
│  │ count                  │  │
│  │ total                  │  │
│  │ average                │  │
│  └────────────────────────┘  │
└──────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The bigger lesson isn't the feedback form.

It's the architecture.

Blockchain applications don't always need to choose between complete transparency and complete centralization.

With confidential execution, an application can keep sensitive state private while still using smart contracts, wallets, and verifiable onchain state.

That opens up a surprisingly large design space for applications that previously didn't make much sense on public blockchains.


What You Can Build From Here

Once the basic feedback box works, several extensions become possible:

Anonymous mode

Allow users to submit feedback without storing their wallet address alongside the feedback.

Multiple surveys

Create separate confidential surveys with different access policies.

Private categories

Store additional confidential metadata such as department, product version, or region.

Encrypted events

Use Sapphire's encrypted-event functionality when an off-chain service needs to consume confidential feedback without exposing the plaintext onchain.

Authenticated dashboards

Add SIWE authentication so authorized users can retrieve confidential records through a frontend.

And that's where this small tutorial becomes a reusable pattern:

Keep raw data confidential. Publish only the information that actually needs to be public.

That is one of the simplest ways to start thinking differently about what an onchain application can be.

Resources

Oasis Sapphire

https://docs.oasis.io/build/sapphire/

Sapphire Developer Documentation

https://docs.oasis.io/build/sapphire/develop/

Sapphire Quickstart

https://docs.oasis.io/build/sapphire/quickstart/

View-Call Authentication

https://docs.oasis.io/build/sapphire/develop/authentication/

Encrypted Events

https://docs.oasis.io/build/sapphire/develop/encrypted-events/

Testing Sapphire Applications

https://docs.oasis.io/build/sapphire/develop/testing/

Top comments (0)