An advanced Web3 tutorial for private product ownership, verifiable warranty decisions, and confidential claim processing.
Introduction
Imagine buying an expensive laptop, camera, bicycle, or other product. Months later, something breaks, and you need to prove that the product is still under warranty.
Today, this process can involve sharing receipts, serial numbers, purchase dates, email addresses, and sometimes identity documents with multiple services. Putting that information directly on a public blockchain would make the situation worse, not better.
What if we could build a warranty system where:
- Customers submit sensitive claim information privately.
- Warranty rules are evaluated by a service running inside a Trusted Execution Environment (TEE).
- The blockchain records the claim's status and a verifiable commitment without exposing the underlying evidence.
- Manufacturers and auditors can verify authorized decisions without receiving every customer's personal information.
In this tutorial, we'll design WarrantyVault, a confidential warranty claims application using Oasis Sapphire, Solidity, TypeScript, and Oasis ROFL.
This is a reference architecture and educational implementation plan. The code snippets illustrate the core design, but the complete production system would require additional integration, testing, and security review.
1. Architecture and technology choices
We will use two Oasis components for different responsibilities.
- Oasis Sapphire: an EVM-compatible confidential smart contract environment for managing claim records, access controls, and claim commitments.
- Oasis ROFL: a framework for running off-chain application logic inside a TEE, suitable for checking signed evidence and interacting with external warranty systems.
Sapphire protects contract state and supports encrypted transactions and queries when clients use the appropriate integration. ROFL provides an attested execution environment and mechanisms for applications to authenticate to on-chain contracts. Neither removes the need to design access control and privacy carefully.
References: Sapphire developer documentation and ROFL documentation.
The system architecture
flowchart TD
A["Customer Web App"] -->|"Encrypted claim submission"| B["Oasis Sapphire"]
B --> C["Confidential Claim Registry"]
D["ROFL Worker in TEE"] -->|"Authenticated request"| B
D --> E["Evidence Verification"]
E --> F["Manufacturer or Retailer API"]
F --> E
E --> D
D -->|"Authorized decision"| B
B --> G["Claim Status"]
G --> A
G --> H["Authorized Auditor"]
The critical design choice is that the customer’s original evidence does not need to be published on-chain. The ROFL worker can validate evidence through a private channel and submit a limited result to the contract.
For example, the contract might store a claim identifier, an evidence commitment, a claim status, and a reference to the authorized verifier. Sensitive documents can remain encrypted outside the chain, with access controlled separately.
2. Define the real use case
Suppose a customer purchases a camera with a two-year manufacturer warranty.
Sixteen months later, the camera stops working. The customer submits:
- A signed or otherwise verifiable proof of purchase.
- The product identifier.
- The purchase date.
- A description of the fault.
- Supporting evidence required by the manufacturer's warranty policy.
The system checks whether the receipt is authentic, the purchase date is valid, the product is covered, and the claim meets the policy requirements.
A successful result does not mean the blockchain has independently proved that the camera is broken. It means the authorized verification process returned a particular decision based on the evidence and policy available to it.
That distinction is essential: a confidential execution environment protects and attests to computation under specific assumptions; it does not magically make false input data true.
3. Repository structure
Start with a monorepo that separates the on-chain contract, user interface, and ROFL worker.
warranty-vault/
├── contracts/
│ ├── src/
│ │ └── WarrantyVault.sol
│ ├── test/
│ │ └── WarrantyVault.test.ts
│ └── hardhat.config.ts
├── frontend/
│ ├── src/
│ │ ├── components/
│ │ ├── lib/
│ │ │ ├── sapphireClient.ts
│ │ │ └── evidence.ts
│ │ └── pages/
│ └── package.json
├── rofl-worker/
│ ├── src/
│ │ ├── worker.ts
│ │ ├── receiptVerifier.ts
│ │ ├── policyEngine.ts
│ │ └── sapphireAdapter.ts
│ ├── Dockerfile
│ └── package.json
├── docs/
│ ├── architecture.md
│ ├── threat-model.md
│ └── deployment.md
├── .env.example
└── README.md
The boundaries are intentional:
-
contracts/contains the claim registry and authorization rules. -
frontend/handles wallet interaction and client-side encryption. -
rofl-worker/validates evidence and evaluates warranty policies. -
docs/records the trust assumptions and deployment procedures.
Never commit private keys, production API credentials, or customer evidence to the repository.
4. Design the claim lifecycle
A simple state machine makes it easier to reason about valid transitions.
stateDiagram-v2
[*] --> Submitted
Submitted --> UnderReview
UnderReview --> Approved
UnderReview --> Rejected
UnderReview --> MoreEvidenceRequired
MoreEvidenceRequired --> UnderReview
Approved --> Closed
Rejected --> Closed
Every transition should have an explicit authorization rule.
For example:
- A customer can submit a claim and provide additional evidence.
- An authorized verifier can start or complete a review.
- Only an authorized policy or governance process can update verifier permissions.
- A closed claim cannot silently return to an approved state.
A production implementation should also define appeal handling, timeouts, policy versions, and what happens when the manufacturer's verification service is unavailable.
5. Implement the Solidity registry
The following contract sketch establishes a minimal record of claim ownership, commitments, status, and verifier authorization.
It deliberately does not emit customer evidence in events. On Sapphire, contract state can be confidential, but events are public by default unless their payloads are explicitly encrypted.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract WarrantyVault {
enum Status {
None,
Submitted,
UnderReview,
Approved,
Rejected,
Closed
}
struct Claim {
address claimant;
bytes32 evidenceCommitment;
bytes32 policyVersion;
Status status;
uint64 submittedAt;
}
address public admin;
mapping(address => bool) public authorizedVerifiers;
mapping(bytes32 => Claim) private claims;
event ClaimStatusChanged(
bytes32 indexed claimId,
Status status
);
modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}
modifier onlyVerifier() {
require(
authorizedVerifiers[msg.sender],
"Not authorized verifier"
);
_;
}
constructor() {
admin = msg.sender;
}
function setVerifier(
address verifier,
bool authorized
) external onlyAdmin {
authorizedVerifiers[verifier] = authorized;
}
function submitClaim(
bytes32 claimId,
bytes32 evidenceCommitment,
bytes32 policyVersion
) external {
require(claimId != bytes32(0), "Invalid claim ID");
require(
claims[claimId].status == Status.None,
"Claim already exists"
);
claims[claimId] = Claim({
claimant: msg.sender,
evidenceCommitment: evidenceCommitment,
policyVersion: policyVersion,
status: Status.Submitted,
submittedAt: uint64(block.timestamp)
});
emit ClaimStatusChanged(
claimId,
Status.Submitted
);
}
function setReviewStatus(
bytes32 claimId,
Status newStatus
) external onlyVerifier {
Claim storage claim = claims[claimId];
require(
claim.status == Status.Submitted ||
claim.status == Status.UnderReview,
"Invalid current status"
);
require(
newStatus == Status.UnderReview ||
newStatus == Status.Approved ||
newStatus == Status.Rejected,
"Invalid next status"
);
claim.status = newStatus;
emit ClaimStatusChanged(claimId, newStatus);
}
function getClaimStatus(
bytes32 claimId
) external view returns (Status) {
return claims[claimId].status;
}
}
Important limitations of this sketch
This contract is not a complete production deployment.
- The administrator is a single address. Production deployments should consider multisignature governance, timelocks, verifier rotation, and recovery procedures.
- The
authorizedVerifiersmapping is a basic allowlist. It does not, by itself, verify a ROFL application's TEE attestation. Integrate the appropriate ROFL authentication mechanism for the target Oasis environment. - The status event exposes the claim identifier and status. Use opaque identifiers and review whether even this metadata should be public. Do not include receipts, names, serial numbers, or private reasons in ordinary events.
- The
submittedAtfield and claim lifecycle may reveal timing information. - The example does not implement appeals, policy upgrades, evidence retrieval, or protection against every possible replay and authorization attack.
Also, Solidity's private keyword is not a privacy mechanism on an ordinary public EVM chain. This architecture relies on deploying and integrating the contract with Sapphire's confidentiality model, with compatible clients for encrypted calls.
Official references: Sapphire concepts and Sapphire security guidance.
6. Create a privacy-preserving evidence commitment
The contract needs a way to bind a claim to the evidence reviewed by the verifier without storing the document itself in public state.
A commitment can be constructed from a canonical representation of the evidence and a secret random salt:
[
C = H(E \,|\, r)
]
Where:
- (C) is the evidence commitment.
- (H) is a cryptographic hash function.
- (E) is the canonicalized evidence representation.
- (r) is a high-entropy random salt.
- (|) means concatenation.
For example, the canonical evidence could contain a receipt digest, product identifier, purchase date, and policy version.
Do not use a predictable value as the salt. Product serial numbers and purchase dates often have limited search spaces. An unsalted hash of such data can allow guessing attacks.
A TypeScript helper might look like this:
import { randomBytes, createHash } from "node:crypto";
type WarrantyEvidence = {
receiptDigest: string;
productId: string;
purchaseDate: string;
policyVersion: string;
};
export function createEvidenceCommitment(
evidence: WarrantyEvidence
) {
const salt = randomBytes(32);
// In production, use a rigorously specified canonical
// serialization shared by every system.
const canonical = JSON.stringify({
receiptDigest: evidence.receiptDigest,
productId: evidence.productId,
purchaseDate: evidence.purchaseDate,
policyVersion: evidence.policyVersion
});
const commitment = createHash("sha256")
.update(canonical, "utf8")
.update(salt)
.digest("hex");
return {
commitment: `0x${commitment}`,
salt: salt.toString("hex")
};
}
This is a simplified illustrative helper. The contract's bytes32 fields require exactly 32-byte values, so the application must encode the hash as bytes correctly before submission.
JSON serialization should not be treated as a universal canonicalization standard. A real system needs a defined schema, stable field ordering and encoding, versioning, and consistent treatment of missing values.
The salt must remain secret until the customer chooses to reveal it. Store it securely alongside the evidence. Publishing both the evidence and its salt would defeat the commitment's privacy purpose.
7. Build the ROFL verification worker
The worker performs checks that should not require exposing the customer's evidence publicly.
A useful pipeline is:
flowchart TD
A["Receive authorized claim request"] --> B["Authenticate request"]
B --> C["Retrieve encrypted evidence"]
C --> D["Verify receipt signature"]
D --> E["Check product and purchase date"]
E --> F["Evaluate versioned warranty policy"]
F --> G{"Evidence valid?"}
G -->|Yes| H["Produce approval decision"]
G -->|No| I["Produce rejection or review decision"]
H --> J["Submit authenticated result"]
I --> J
J --> K["Update Sapphire claim status"]
The worker should produce a narrowly scoped decision instead of copying sensitive evidence into the blockchain.
An illustrative policy function could be:
type VerificationInput = {
receiptSignatureValid: boolean;
productCovered: boolean;
purchaseDateValid: boolean;
policyVersion: string;
};
type Decision = "approved" | "rejected" | "manual_review";
export function evaluateWarranty(
input: VerificationInput
): Decision {
if (!input.receiptSignatureValid) {
return "manual_review";
}
if (!input.productCovered || !input.purchaseDateValid) {
return "rejected";
}
return "approved";
}
This is a policy demonstration, not a complete receipt-verification implementation. Real receipt authentication requires a defined issuer, trusted public keys, signature validation, key rotation, and handling for revoked or compromised keys.
The worker must also handle network failures, duplicate requests, expired evidence, and retries. Use an idempotent claim identifier and a policy version so that retrying a request cannot accidentally create a different decision.
How should ROFL authenticate?
Do not treat a hard-coded worker address as proof that the request came from a genuine attested application.
For a real deployment, register and manage the ROFL application using the supported Oasis workflow, and integrate the documented authentication mechanism between the ROFL application and the Sapphire contract. Verify the app identity and its authorized permissions before accepting a decision.
Start with the ROFL workflow documentation and the ROFL build guide.
8. Integrate the frontend with Sapphire
The frontend has three responsibilities:
- Collect the minimum information needed for a claim.
- Encrypt sensitive inputs using a supported Sapphire client integration.
- Display claim status without unnecessarily retrieving or exposing private evidence.
The conceptual submission flow is:
// Architectural pseudocode, not a complete Sapphire client.
async function submitWarrantyClaim(
evidence: WarrantyEvidence
) {
const { commitment, salt } =
createEvidenceCommitment(evidence);
// 1. Encrypt the sensitive payload using the
// supported Sapphire client integration.
//
// 2. Store encrypted evidence in the chosen
// access-controlled evidence store.
//
// 3. Submit the claim commitment through a
// Sapphire-compatible encrypted transaction.
//
// 4. Securely preserve the salt and evidence reference.
return {
commitment,
salt,
nextStep: "Submit through the configured Sapphire client"
};
}
The example deliberately leaves out the transaction call because the exact implementation depends on the selected ethers or viem integration, wallet, network configuration, and contract ABI.
Use the official Sapphire client documentation to select a supported client package. Ordinary contract calls are not automatically end-to-end encrypted simply because the destination is Sapphire.
For evidence stored outside the chain, use authenticated encryption, access controls, and a clear retention policy. Encryption at rest alone is insufficient if every application server can freely retrieve plaintext.
9. Security and threat model
A privacy-preserving warranty system still has meaningful trust assumptions.
| Threat | Mitigation |
|---|---|
| Receipt forgery | Verify issuer signatures and trusted signing keys |
| Evidence exposure | Encrypt client submissions and restrict evidence access |
| Malicious verifier | Authenticate ROFL and restrict contract permissions |
| Replayed decision | Bind decisions to claim ID, policy version, and current status |
| Administrator compromise | Multisig governance, timelocks, and key rotation |
| Metadata leakage | Minimize public identifiers, event payloads, and timing correlations |
| Incorrect policy | Version policy logic, test edge cases, and support appeals |
| TEE vulnerabilities | Document hardware, attestation, software, and operator assumptions |
| External API compromise | Authenticate responses and independently verify signed records |
Sapphire does not automatically hide all metadata. Transaction timing, public addresses, event topics, state access patterns, and other observable behavior may still reveal information. Its documentation specifically warns that state-access patterns and contract logs can undermine privacy.
ROFL also does not prove that a warranty policy is fair or that a manufacturer supplied truthful information. Attestation helps establish evidence about the execution environment and application identity under defined assumptions; it is not a universal proof of correctness.
10. Testing strategy
Before a testnet deployment, write tests for the security properties rather than testing only the happy path.
Contract tests
- A customer cannot overwrite an existing claim.
- An unauthorized address cannot approve or reject a claim.
- Invalid state transitions revert.
- Claim identifiers cannot be reused to replace an existing record.
- Changing verifier permissions affects only the intended addresses.
Worker tests
- Invalid receipt signatures do not result in automatic approval.
- An expired warranty is rejected according to the selected policy.
- Missing manufacturer API responses trigger retries or manual review.
- Repeated requests produce consistent, idempotent outcomes.
- Decisions are bound to the correct claim and policy version.
Privacy tests
- No ordinary event contains receipt contents, serial numbers, names, or other sensitive evidence.
- The client uses the appropriate Sapphire encryption wrapper.
- Salts and encryption keys never appear in application logs.
- Unauthorized users cannot retrieve private evidence through the API.
- Public status queries reveal only the information the product intentionally exposes.
Use Sapphire's testing guidance as a starting point: Sapphire testing documentation.
11. Deployment roadmap
A sensible implementation sequence is:
- Implement and test the claim registry on a local development setup.
- Configure the frontend to use a supported Sapphire client and encrypted transactions.
- Implement evidence canonicalization, commitment generation, and encrypted evidence storage.
- Build the ROFL worker with signature verification and a versioned policy engine.
- Register the ROFL application and integrate the documented authentication mechanism.
- Deploy the components to Sapphire Testnet and test both successful and adversarial flows.
- Review privacy leakage, permissions, key management, operational monitoring, and recovery procedures.
- Obtain an independent security review before handling real customer evidence or warranty decisions.
The repository should include setup instructions, network configuration, sample test evidence, a threat model, and explicit warnings against using production personal data during development.
Conclusion
WarrantyVault illustrates a useful class of Web3 applications: systems where the blockchain provides an auditable record of decisions, but the information needed to make those decisions should not be public.
Sapphire can provide confidential smart contract execution and state, while ROFL can support private off-chain verification in an attested environment. Together, they offer a foundation for applications that need both accountability and discretion.
The most important engineering lesson is that confidentiality is an end-to-end property. It depends on the client, evidence storage, contract, events, worker, external data sources, and operational controls working together.
For warranty claims, that means proving the right thing without publishing everything about the person making the claim.
Top comments (0)