Building a Confidential Bug Bounty Platform with Oasis Sapphire
An advanced tutorial on private vulnerability reports, confidential triage, and verifiable bounty payouts
Bug bounty platforms have a difficult security problem.
A researcher discovers a vulnerability in a smart contract and submits a report. The report may contain exploit steps, proof-of-concept code, affected endpoints, or details that would allow someone else to reproduce the issue.
If the report becomes public too early, the disclosure process can turn into an attacker's instruction manual.
But making everything private creates another problem: researchers need confidence that their submissions will be timestamped, evaluated fairly, and rewarded according to clear rules.
This is an interesting use case for Oasis Sapphire.
Sapphire provides an EVM-compatible environment for confidential smart contract state and execution. Instead of putting sensitive report contents directly into public contract storage or events, we can design a system where reports remain confidential while submission commitments, bounty rules, and final payout records can be independently verified.
In this tutorial, we'll design an advanced confidential bug bounty platform using Solidity and Oasis Sapphire.
We'll cover:
- Confidential vulnerability submissions
- Commitments that provide evidence of a report's integrity
- Private triage and severity assessment
- Bounty policy enforcement
- Duplicate-submission handling
- Controlled disclosure
- Verifiable payout records
- Threat modeling and production considerations
The key design principle is simple:
Keep the vulnerability confidential until disclosure is safe, but make the bounty process accountable from the beginning.
1. Architecture: Separate the Report from Its Proof
The first mistake would be to put the entire vulnerability report onchain.
Even if a Solidity variable is marked private, that keyword does not provide confidentiality on an ordinary public EVM chain. Sapphire's confidential execution environment gives us a different foundation, but we still need to avoid accidentally leaking sensitive data through events, public interfaces, or transaction metadata.
We'll separate the system into three components.
flowchart TD
R[Security Researcher] -->|Encrypted submission| S[Sapphire Contract]
S --> C[Confidential Report State]
S --> P[Public Commitment and Status]
T[Authorized Triage Team] -->|Authenticated review| S
S -->|Approved decision| B[Bounty Policy]
B -->|Verified payout authorization| W[Treasury or Payout Contract]
W --> L[Public Settlement Record]
The report contains the technical details of the vulnerability. The commitment gives the researcher evidence that a specific report was submitted. The payout record makes the financial outcome auditable without publishing the exploit itself.
These are separate guarantees.
A commitment does not prove that a report is valid. A confidential contract does not automatically make every transaction field private. A payout record does not prove that the triage decision was correct.
The system must make those boundaries explicit.
2. Define the Submission Lifecycle
A bounty platform needs a lifecycle that prevents reports from jumping directly from submission to payout.
We'll use the following states:
stateDiagram-v2
[*] --> Submitted
Submitted --> UnderReview
UnderReview --> Duplicate
UnderReview --> Rejected
UnderReview --> Accepted
Accepted --> RewardAuthorized
RewardAuthorized --> Paid
Accepted --> DisclosureReady
Paid --> DisclosureReady
DisclosureReady --> Disclosed
A real implementation should define whether payment is required before disclosure, how disputed reports are handled, and what happens if a project never responds.
For this tutorial, the important distinction is between report confidentiality and process transparency. We can keep the report private while exposing a limited, non-sensitive status and eventual settlement record.
3. Project Structure
We'll use Solidity, Hardhat, and TypeScript.
confidential-bounty/
├── contracts/
│ ├── ConfidentialBounty.sol
│ ├── BountyTreasury.sol
│ └── interfaces/
│ └── IBountyPolicy.sol
├── scripts/
│ ├── deploy.ts
│ └── configure-policy.ts
├── test/
│ ├── ConfidentialBounty.ts
│ └── BountyTreasury.ts
├── app/
│ ├── submit-report.ts
│ └── reviewer-client.ts
├── docs/
│ ├── threat-model.md
│ └── disclosure-policy.md
├── hardhat.config.ts
└── package.json
The contract handles submission state and policy enforcement. The client is responsible for encrypting and submitting the report through a Sapphire-compatible transaction flow. The treasury handles settlement separately so the bounty system doesn't need unrestricted custody of funds.
4. Model the Report Carefully
A report needs more than a string.
At minimum, it needs an identifier, a commitment, a submitter, a status, and a bounty policy version.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract ConfidentialBounty {
enum Status {
None,
Submitted,
UnderReview,
Duplicate,
Rejected,
Accepted,
RewardAuthorized,
Paid,
Disclosed
}
struct Report {
bytes32 commitment;
address researcher;
Status status;
uint32 policyVersion;
uint64 submittedAt;
}
mapping(bytes32 => Report) private reports;
event ReportRegistered(
bytes32 indexed reportId,
bytes32 commitment,
uint32 policyVersion
);
function registerReport(
bytes32 reportId,
bytes32 commitment,
uint32 policyVersion
) external {
require(reportId != bytes32(0), "Invalid report ID");
require(commitment != bytes32(0), "Invalid commitment");
require(
reports[reportId].status == Status.None,
"Report already exists"
);
reports[reportId] = Report({
commitment: commitment,
researcher: msg.sender,
status: Status.Submitted,
policyVersion: policyVersion,
submittedAt: uint64(block.timestamp)
});
emit ReportRegistered(
reportId,
commitment,
policyVersion
);
}
}
This is a starting point, not a complete production contract.
The example deliberately keeps the report body out of the event. The commitment can help establish that a particular report corresponds to a registered submission, but the report ID and submitter address are still visible in the event.
If submitter identity or submission timing is sensitive, even this metadata requires additional design.
On Sapphire, the confidential report body should be stored and processed through confidential contract state, while the public interface should reveal only information that the disclosure policy explicitly permits.
5. Commit to the Report Without Publishing It
How can a researcher prove that a report hasn't been silently replaced after submission?
A useful starting point is a cryptographic commitment.
Let:
-
reportbe the complete report content -
saltbe a high-entropy random secret -
versionidentify the report format
The researcher computes:
commitment = keccak256(
encode(version, report, salt)
)
The commitment can be registered with the report ID. Later, the researcher can reveal the report and salt to demonstrate that the revealed content matches the original commitment.
However, there is a critical caveat.
A hash does not automatically protect low-entropy information. If the report is predictable, someone may guess candidate contents and compare their hashes. The random salt must be generated securely and kept secret until disclosure.
The client should use a canonical encoding format, and the contract should define exactly which fields are included in the commitment. Otherwise, different encodings of the same logical report may produce inconsistent commitments.
For reports containing confidential source code or exploit details, the commitment should not replace encrypted storage. It provides an integrity mechanism, not a confidentiality mechanism.
6. Enforce Bounty Rules with Versioned Policies
Bounty programs often change their reward limits, supported targets, and severity definitions.
If a policy changes while a report is under review, which version should apply?
The answer should be explicit.
Each report should reference a policy version that was active when it was submitted. The policy should define:
- Eligible contracts or systems
- Severity categories
- Reward ranges
- Duplicate-report rules
- Exclusions
- Disclosure requirements
- Review deadlines
For example:
struct BountyPolicy {
uint128 criticalMax;
uint128 highMax;
uint128 mediumMax;
uint128 lowMax;
uint64 reviewDeadline;
bool active;
}
mapping(uint32 => BountyPolicy) private policies;
In production, changing a policy should create a new version rather than silently rewriting the terms associated with existing reports.
The contract should validate that the selected policy version exists and that the report references a version permitted by the program.
The reward authorization process should also verify that the report is in an eligible state, the proposed reward is within the relevant limit, and the request has not already been used.
7. Keep Triage Confidential, but Make It Accountable
A confidential report can contain information that only a small review team should access.
That does not mean the triage process should be an unrestricted black box.
A production design should separate the permissions to:
- Read a report.
- Change its status.
- Assign a severity.
- Authorize a reward.
- Approve disclosure.
These permissions should not automatically belong to the same address.
A multisig or governance-controlled role can manage reviewer membership, while a dedicated authorization path enforces reward limits. For more complex offchain analysis, a ROFL application could perform selected computations inside a trusted execution environment and provide evidence about the computation's origin.
ROFL should not be treated as a magical correctness oracle. Remote attestation can provide evidence about the environment and code being executed, but it does not prove that the underlying vulnerability assessment is objectively correct.
A reviewer still needs sound security reasoning, and the protocol needs a clear process for disputes.
8. A Safer Disclosure Workflow
Disclosure is one of the most important parts of the system.
A report should not become public just because its status changes to Accepted.
Instead, define a separate disclosure process with explicit conditions:
sequenceDiagram
participant R as Researcher
participant C as Sapphire Contract
participant V as Reviewer
participant T as Treasury
participant P as Project Maintainer
R->>C: Submit encrypted report
C-->>R: Report ID and commitment
V->>C: Request authorized review
V->>C: Record review decision
C->>C: Check policy and status
C->>T: Authorize eligible reward
T-->>R: Pay researcher
P->>P: Patch and validate vulnerability
P->>C: Confirm disclosure readiness
R->>P: Coordinate responsible disclosure
This is a conceptual workflow. A production system must implement authenticated reviewer access, authorization checks, deadlines, dispute resolution, and secure report delivery.
In particular, a public function that returns a report body would undermine confidentiality. Reviewer access should use Sapphire's supported confidential call and authentication patterns, and sensitive report data should never be emitted in ordinary public events.
9. Threat Model
Before deployment, consider at least the following threats.
| Threat | Mitigation |
|---|---|
| Report contents leak through events | Emit only approved metadata |
| Report is replaced after submission | Use a canonical, salted commitment |
| Researcher submits the same report repeatedly | Define duplicate rules and use stable report identifiers |
| Reviewer abuses privileged access | Separate roles, log approved actions, and limit permissions |
| Reward is paid twice | Use unique authorization IDs and replay protection |
| Policy changes mid-review | Pin each report to an explicit policy version |
| Exploit becomes public too early | Define access control and staged disclosure |
| Metadata reveals a sensitive incident | Minimize public fields and evaluate timing leakage |
| ROFL computation is treated as unquestionable | Separate attestation evidence from correctness claims |
Confidential computing also has limitations. Transaction timing, gas usage, input size, and observable state transitions can reveal information. Sapphire developers should review the platform's security guidance and avoid assuming that encrypted state eliminates every side channel.
10. What Makes This Useful Beyond Bug Bounties?
The same design pattern can support other applications that need confidential submissions and accountable decisions:
- Private security audits
- Confidential vulnerability disclosure
- Responsible AI incident reporting
- Private compliance reports
- Confidential grant applications
- Whistleblower submission systems
In each case, the core problem is similar: sensitive information must be available to authorized reviewers without becoming public by default, while the process needs evidence, permissions, and a clear outcome.
The most important architectural decision is to separate the confidential content, the public evidence, and the authority to act on a submission.
Oasis Sapphire provides a confidential EVM environment for building this kind of application. ROFL can extend the architecture when verifiable offchain computation is useful. Neither removes the need for careful access control, cryptographic design, or a well-defined disclosure process.
Conclusion
A bug bounty platform is a useful example of why confidentiality and transparency don't have to be opposites.
The vulnerability report can remain confidential. The submission commitment can provide evidence of integrity. The policy can constrain the reward. The settlement can be independently verified. And disclosure can happen only when the appropriate conditions are met.
That is a much more useful model than publishing every detail onchain or asking everyone to trust a private database.
The goal is not to make the entire process invisible.
It is to make sensitive information private while keeping important decisions accountable.
Resources
- Oasis Sapphire: https://docs.oasis.io/build/sapphire/
- Sapphire developer documentation: https://docs.oasis.io/build/sapphire/develop/
- Oasis ROFL: https://docs.oasis.io/build/rofl/
- Sapphire encrypted events: https://docs.oasis.io/build/sapphire/develop/encrypted-events/
- Oasis security documentation: https://docs.oasis.io/build/sapphire/develop/security/
Top comments (0)