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
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
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
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;
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";
This allows the Hardhat environment to apply Sapphire's transaction handling automatically.
3. Write the Feedback Contract
Create:
contracts/FeedbackBox.sol
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;
}
}
}
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;
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;
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);
It tells observers that an address submitted feedback.
But this would be a bad idea:
event FeedbackSubmitted(
address indexed user,
uint8 rating,
string comment
);
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
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;
});
Compile:
npx hardhat compile
Then deploy to Sapphire Testnet:
npx hardhat run scripts/deploy.ts \
--network sapphire-testnet
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");
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);
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()
and retrieve:
Submissions: 143
Total rating: 587
Average: 4
But that doesn't reveal every individual comment.
Conceptually:
Feedback
│
┌─────────┴─────────┐
│ │
▼ ▼
Private state Aggregates
│ │
│ └── count
│ └── total
│ └── average
│
└── rating
└── comment
└── timestamp
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 (...);
}
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
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"
);
});
});
Run:
npx hardhat test
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) {
...
}
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?
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?
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 │ │
│ └────────────────────────┘ │
└──────────────────────────────┘
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
Top comments (0)