What if a DeFi application could evaluate a wallet's creditworthiness without publishing the credit score, risk thresholds, underwriting rules, or internal decision variables on-chain?
That is the problem we are going to solve.
In this tutorial, we will build a confidential on-chain credit underwriting engine using Oasis Sapphire, Oasis ROFL, and Base.
The application will have a deliberately split architecture:
- Base remains the public source of collateral and lending activity.
- ROFL collects public blockchain data and performs the risk calculation inside a Trusted Execution Environment.
- Sapphire stores the confidential underwriting state.
- A Sapphire smart contract decides whether a borrower is eligible for a credit limit.
- The borrower's exact score, policy thresholds, and internal risk parameters are never published as ordinary blockchain state.
- The final authorization can be consumed by another application without exposing the complete underwriting model.
This is not intended to be a production lending protocol. It is an architectural reference showing how confidential computation can be combined with public blockchain state.
The interesting part is not hiding the blockchain.
The interesting part is deciding which parts should be public, which parts should be confidential, and which results should be verifiable without revealing the inputs that produced them.
1. The Problem
Imagine a crypto-native merchant financing protocol.
A merchant deposits collateral on Base and wants access to a short-term credit line.
A traditional DeFi lending protocol might calculate something like:
collateral value
+
borrowed value
+
liquidation history
+
wallet activity
=
credit limit
The problem is that the underwriting process itself can become public information.
Suppose the protocol uses:
minimum_score = 720
max_utilization = 65%
max_drawdown = 30%
risk_weight = 0.42
and publishes all of those values on-chain.
An adversary can now reverse-engineer the protocol.
Worse, if the protocol stores a user's individual score publicly:
wallet:
0x1234...
score:
783
creditLimit:
$42,000
then the blockchain becomes a public credit database.
That is undesirable for several reasons.
A borrower may not want every observer to know their risk classification.
A protocol may not want competitors to know its underwriting methodology.
A risk provider may not want its proprietary model exposed.
And a sophisticated attacker may deliberately optimize behavior against the published thresholds.
The goal, therefore, is not:
"Make the blockchain private."
The goal is:
Keep the source data and underwriting logic confidential while publishing only the minimum information required to authorize a financial action.
That is a much more interesting architecture.
2. What We Are Building
Our example protocol will be called PrivateCredit.
A user interacts with the system through a normal wallet.
The user has collateral and activity on Base.
The application periodically requests an underwriting decision.
The ROFL worker reads public blockchain information, calculates a risk profile inside its TEE, and submits the resulting authorization to Sapphire.
Sapphire stores the confidential result and exposes only narrowly scoped functions to consuming applications.
The simplified flow looks like this:
flowchart LR
U[Borrower Wallet]
B[Base]
R[ROFL Risk Engine]
S[Sapphire]
C[Credit Controller]
L[Lending / Merchant App]
U -->|Collateral + activity| B
B -->|Public blockchain state| R
R -->|Confidential underwriting result| S
S --> C
C -->|Authorized credit limit| L
L -->|Credit action| U
There is an important boundary here.
Base does not become confidential.
Anyone can still inspect the borrower's public transactions.
Instead, we are protecting the derived intelligence.
That distinction is fundamental.
3. Why ROFL + Sapphire?
Sapphire is an EVM-compatible confidential runtime designed for confidential smart-contract state, encrypted transactions, and confidential computation.
ROFL extends that model to off-chain workloads.
A ROFL application executes inside a Trusted Execution Environment and can interact with external systems while retaining confidentiality and producing authenticated interactions with the Oasis ecosystem.
That gives us two different computational environments.
Sapphire
Use Sapphire for:
- confidential contract state
- authorization
- encrypted inputs
- access-controlled outputs
- deterministic policy enforcement
ROFL
Use ROFL for:
- fetching Base data
- aggregating many transactions
- calling external APIs
- running more expensive calculations
- maintaining private model parameters
- processing data that would be awkward or expensive on-chain
This separation is important.
You don't want to perform a large historical risk calculation directly inside a smart contract.
And you don't want the final authorization decision to exist only inside an off-chain server.
So we use both.
4. Threat Model
Before writing code, define what we are protecting.
Our adversaries include:
Blockchain observers
They can see:
- Base transactions
- contract addresses
- public balances
- public collateral
- public borrowing activity
We cannot hide those from someone who can inspect Base.
ROFL host/operator
The application executes inside a TEE.
The architecture therefore relies on the TEE and its attestation model rather than assuming that the underlying host operator should see the application's secrets.
ROFL's security model is based around TEEs, remote attestation, Oasis consensus, and registered applications.
Application users
Users should not automatically receive:
- the complete risk model
- other users' scores
- internal thresholds
- historical underwriting decisions
Our own frontend
The frontend should not become a privileged database.
This is why sensitive state belongs in the confidential execution layer rather than simply being hidden behind a web API.
5. What We Are NOT Protecting
This is equally important.
If a user's collateral is on Base, the collateral transaction is public.
If the user interacts with a public lending protocol, those interactions are public.
Sapphire does not magically erase public-chain history.
Our confidentiality boundary begins when public information is ingested into the underwriting system.
Think about the pipeline like this:
PUBLIC
|
| Base transactions
v
+--------------------+
| ROFL TEE |
| |
| private features |
| risk model |
| policy parameters |
+--------------------+
|
| authorized result
v
+--------------------+
| Sapphire |
| |
| private score |
| credit limit |
| expiry |
+--------------------+
|
| minimal output
v
CONSUMING APPLICATION
That is the architecture we want.
6. Repository Structure
The repository for the tutorial can be organized like this:
private-credit/
│
├── contracts/
│ ├── PrivateCredit.sol
│ ├── CreditController.sol
│ ├── PolicyRegistry.sol
│ └── interfaces/
│ └── IPrivateCredit.sol
│
├── rofl/
│ ├── src/
│ │ ├── index.ts
│ │ ├── base.ts
│ │ ├── scoring.ts
│ │ ├── features.ts
│ │ └── sapphire.ts
│ │
│ ├── Dockerfile
│ └── package.json
│
├── scripts/
│ ├── deploy.ts
│ ├── seed.ts
│ └── request-underwriting.ts
│
├── test/
│ ├── PrivateCredit.t.sol
│ ├── PolicyRegistry.t.sol
│ └── integration.t.sol
│
├── frontend/
│ └── ...
│
├── docker-compose.yml
├── hardhat.config.ts
├── foundry.toml
└── README.md
The important design decision is that the risk engine and the smart contract are separate components.
The smart contract does not trust arbitrary off-chain results.
It verifies that the caller is an authorized ROFL application.
7. The Confidential Contract
Let's start with the Sapphire contract.
The contract needs to maintain a confidential credit record.
Conceptually:
struct CreditRecord {
uint256 limit;
uint256 score;
uint64 expiresAt;
uint64 nonce;
bool active;
}
We deliberately do not emit the score in a normal public event.
Instead, the record remains inside Sapphire's confidential state.
A simplified contract looks like this:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract PrivateCredit {
struct CreditRecord {
uint256 limit;
uint256 score;
uint64 expiresAt;
uint64 nonce;
bool active;
}
mapping(address => CreditRecord) private records;
address public roflApp;
modifier onlyROFL() {
require(msg.sender == roflApp, "not authorized");
_;
}
constructor(address _roflApp) {
roflApp = _roflApp;
}
function updateCredit(
address borrower,
uint256 score,
uint256 limit,
uint64 expiresAt
) external onlyROFL {
CreditRecord storage record = records[borrower];
record.score = score;
record.limit = limit;
record.expiresAt = expiresAt;
record.nonce++;
record.active = true;
}
function revokeCredit(
address borrower
) external onlyROFL {
records[borrower].active = false;
}
function getCreditLimit(
address borrower
) external view returns (uint256) {
CreditRecord memory record = records[borrower];
require(record.active, "inactive");
require(block.timestamp <= record.expiresAt, "expired");
return record.limit;
}
}
This is intentionally simplified.
A production version should use Sapphire's recommended mechanisms for authenticating ROFL-originated calls rather than treating an ordinary msg.sender check as sufficient.
The important architectural idea is that the authorization boundary belongs to the confidential runtime.
ROFL's integration with Sapphire allows contracts to verify that transactions originate from registered ROFL applications.
8. Don't Store a "Global Score"
One easy mistake would be to create:
mapping(address => uint256) public score;
and call it a privacy solution.
It isn't.
The public getter would make the value readable.
Even without public, you need to carefully consider which functions expose confidential information.
Instead, the application should expose purpose-specific queries.
For example:
function canBorrow(
address borrower,
uint256 amount
) external view returns (bool) {
CreditRecord memory record = records[borrower];
if (!record.active) {
return false;
}
if (block.timestamp > record.expiresAt) {
return false;
}
return amount <= record.limit;
}
The consuming application learns:
true
rather than:
score = 813
limit = $47,391
riskWeight = 0.371
This is a very important privacy pattern:
Expose decisions rather than sensitive intermediate state whenever possible.
9. The Feature Pipeline
Now we move to ROFL.
The risk engine needs public information.
Suppose our underwriting model uses:
wallet_age
transaction_count
collateral_value
borrow_utilization
liquidation_count
repayment_ratio
portfolio_volatility
The ROFL application can query Base RPC endpoints and construct a feature vector.
For example:
export interface BorrowerFeatures {
walletAgeDays: number;
transactionCount: number;
collateralUsd: number;
utilization: number;
liquidations: number;
repaymentRatio: number;
volatility: number;
}
The feature extraction layer should be deterministic.
That makes debugging substantially easier.
export function normalizeFeatures(
raw: BorrowerFeatures
) {
return {
walletAge: Math.min(raw.walletAgeDays / 3650, 1),
activity: Math.min(raw.transactionCount / 1000, 1),
collateral: Math.min(raw.collateralUsd / 100000, 1),
utilization: Math.min(raw.utilization, 1),
liquidations: Math.min(raw.liquidations / 10, 1),
repayment: raw.repaymentRatio,
volatility: Math.min(raw.volatility, 1)
};
}
Notice that none of this is performed on Base.
It happens inside the ROFL application.
10. The Risk Model
For this tutorial, use a deliberately simple model.
The point isn't to invent a sophisticated credit algorithm.
The point is to demonstrate where the algorithm lives.
export function calculateScore(
f: ReturnType<typeof normalizeFeatures>
): number {
let score = 500;
score += f.walletAge * 100;
score += f.activity * 75;
score += f.collateral * 125;
score += f.repayment * 150;
score -= f.utilization * 150;
score -= f.liquidations * 200;
score -= f.volatility * 100;
return Math.max(
0,
Math.min(1000, Math.round(score))
);
}
A production system would likely use a much more sophisticated model.
It might include:
- multiple assets
- liquidation distance
- realized volatility
- counterparty exposure
- stablecoin concentration
- protocol-specific risk
- historical repayment behavior
- oracle confidence
- time-weighted collateral
- anomaly detection
But the important thing is that the model does not need to become blockchain state.
11. Keeping the Policy Confidential
Now comes the interesting part.
Suppose the protocol has these rules:
const POLICY = {
minimumScore: 720,
maximumUtilization: 0.68,
maximumLimitUsd: 50000,
liquidationPenalty: 180,
scoreVersion: "2026-09"
};
Publishing this policy would allow users to optimize directly against it.
Instead, it remains inside the ROFL application.
The underwriting function becomes:
export function underwrite(
features: BorrowerFeatures
) {
const normalized = normalizeFeatures(features);
const score = calculateScore(normalized);
if (score < POLICY.minimumScore) {
return {
approved: false,
score
};
}
const utilizationPenalty =
normalized.utilization * POLICY.maximumLimitUsd;
const limit =
Math.max(
0,
Math.floor(
POLICY.maximumLimitUsd - utilizationPenalty
)
);
return {
approved: true,
score,
limit
};
}
Again, this is illustrative rather than production-grade underwriting.
The important part is the confidentiality boundary.
The user does not need to know:
minimumScore = 720
They only need to know:
approved = true
or:
approved = false
12. Why Not Just Use a Server?
A reasonable question is:
Why not put this behind an HTTPS API?
You could.
But then the trust model becomes:
user
|
v
centralized server
|
v
database
The user must trust the server operator to:
- run the correct code
- protect the database
- protect API credentials
- not alter decisions
- not leak scores
- not secretly change policy
ROFL gives us a different architecture.
user
|
v
Oasis network
|
+--> registered ROFL application
|
v
TEE
|
v
risk engine
The ROFL model is specifically designed for arbitrary off-chain computation that can interact with external resources while retaining confidentiality and providing verifiable integration with the Oasis ecosystem.
It doesn't mean "trust disappears."
It means the trust assumptions become explicit and tied to the TEE, attestation, application registration, and Oasis infrastructure.
That distinction matters.
13. Reading Base from ROFL
The ROFL application needs an RPC connection.
import { createPublicClient, http } from "viem";
import { base } from "viem/chains";
const baseClient = createPublicClient({
chain: base,
transport: http(process.env.BASE_RPC_URL)
});
We can then inspect contracts.
For example:
const collateral = await baseClient.readContract({
address: COLLATERAL_ADDRESS,
abi: collateralAbi,
functionName: "balanceOf",
args: [borrower]
});
The key architectural point is that the ROFL application can communicate with external networks.
ROFL applications can use authenticated network connections and can incorporate light clients or HTTPS/TLS connections depending on the application.
That makes ROFL especially useful for applications where the input data does not originate entirely inside Oasis.
14. Building the Underwriting Worker
The worker ties everything together.
async function processBorrower(
borrower: `0x${string}`
) {
const raw = await collectFeatures(
borrower
);
const decision = underwrite(raw);
if (!decision.approved) {
await revokeCredit(borrower);
return;
}
await updateCredit(
borrower,
decision.score,
decision.limit
);
}
The production version should additionally include:
- idempotency
- nonce handling
- stale-data detection
- retry logic
- RPC failover
- transaction confirmation
- policy versioning
- model versioning
- attestation-aware deployment
- monitoring
The simple worker is enough to demonstrate the architecture.
15. Never Let a Stale Risk Decision Live Forever
One of the easiest mistakes in automated credit systems is forgetting that risk changes.
Suppose a borrower receives:
creditLimit = $40,000
at 12:00.
At 12:10, their collateral collapses.
The previous authorization should not remain valid indefinitely.
This is why our record contains:
uint64 expiresAt;
The consuming application can enforce:
require(
block.timestamp <= record.expiresAt,
"credit expired"
);
This creates a temporal trust boundary.
Instead of:
"This address is trusted."
we have:
"This address was authorized for this amount until this time."
That is substantially safer.
16. Model Versioning
Now consider the underwriting model itself.
Suppose version 1 is:
model = credit-v1
and version 2 changes the scoring function.
If the system does not track model versions, historical decisions become difficult to interpret.
So add:
struct CreditRecord {
uint256 limit;
uint256 score;
uint64 expiresAt;
uint64 nonce;
bytes32 policyHash;
bytes32 modelHash;
bool active;
}
The score remains confidential.
But the cryptographic identifiers of the policy/model can be retained as provenance metadata.
For example:
modelHash:
0x9f83...
policyHash:
0x72a1...
This gives you an important property:
The implementation can change without making historical authorization impossible to audit.
You can distinguish:
What was decided?
from:
Which policy generated that decision?
without necessarily publishing the policy itself.
17. A Better Architecture
At this point our architecture becomes:
flowchart TB
BASE[Base]
RPC[Base RPC / Indexer]
ROFL[ROFL TEE]
FEATURES[Feature Extraction]
MODEL[Private Risk Model]
POLICY[Private Policy]
SAPPHIRE[Sapphire]
CREDIT[Private Credit Controller]
APP[Merchant / Lending App]
BASE --> RPC
RPC --> ROFL
ROFL --> FEATURES
FEATURES --> MODEL
MODEL --> POLICY
POLICY -->|Decision| SAPPHIRE
SAPPHIRE --> CREDIT
CREDIT --> APP
There is a subtle but important property here.
The public blockchain is used as the source of evidence.
It is not used as the storage layer for private underwriting state.
18. Confidential Events
Sometimes an application needs event-driven architecture.
For example:
event CreditDecision(
address indexed borrower,
uint256 limit
);
That would leak the limit.
Instead, Sapphire supports encrypted event payloads while keeping event topics available for indexing and off-chain triggering.
That lets us design an event such as:
topic:
CreditDecision(address)
encrypted payload:
{
limit,
score,
expiresAt,
modelHash
}
An authorized consumer can decrypt the payload.
An arbitrary blockchain observer cannot simply read the sensitive values from the event.
This is a useful pattern for systems that still need event-driven infrastructure without turning every event into a public data leak.
19. Don't Encrypt Everything
A common privacy-design mistake is assuming:
More encryption = better architecture.
Not necessarily.
Some data should remain public.
For example:
CreditController deployed
ROFL application registered
Policy version changed
Model version changed
Credit authorization revoked
These facts can often be public.
What should remain confidential are values such as:
individual score
risk features
private policy thresholds
exact credit limit
sensitive model parameters
The correct architecture is therefore:
PUBLIC METADATA
+
CONFIDENTIAL STATE
+
MINIMAL DISCLOSURE
not:
ENCRYPT EVERYTHING
20. Authorization Is More Important Than Encryption
Suppose we correctly encrypt the credit record.
But then we expose:
function getScore(address borrower)
external
returns (uint256)
Privacy is gone.
This is why confidentiality has to be treated as an application-level authorization problem, not just a cryptography problem.
Ask:
- Who can write the state?
- Who can read the state?
- Who can cause a decision?
- Who can revoke a decision?
- What can a caller infer from success/failure?
- What information leaks through events?
- What information leaks through timing?
- What information leaks through transaction metadata?
The last question is particularly important.
A confidential contract does not automatically make the surrounding application metadata confidential.
21. The Credit Controller
Now suppose a lending application wants to check whether a borrower can draw $10,000.
It doesn't need the score.
It needs a decision.
function canDraw(
address borrower,
uint256 amount
) external view returns (bool) {
CreditRecord memory record =
records[borrower];
if (!record.active) {
return false;
}
if (block.timestamp > record.expiresAt) {
return false;
}
return amount <= record.limit;
}
The result is:
true
The application can then execute the action.
This pattern is called capability-oriented disclosure.
Instead of revealing the entire internal state, you reveal only whether a particular operation is authorized.
That is a much better primitive for privacy-sensitive financial applications.
22. What Happens When the User Is Rejected?
Do not assume that returning:
score = 694
is harmless.
A user can repeatedly modify their behavior and probe the model.
If every request reveals a numerical score, they can potentially infer the decision boundary.
A better interface is:
eligible = false
possibly combined with a coarse reason category:
INSUFFICIENT_COLLATERAL
or:
TEMPORARILY_INELIGIBLE
Even these should be designed carefully.
The goal is to minimize the information returned by the underwriting oracle.
23. Replay Protection
ROFL may process the same borrower more than once.
Transactions can be retried.
RPC responses can be delayed.
Workers can restart.
Therefore, the credit contract needs a monotonic nonce.
require(
nonce > records[borrower].nonce,
"stale decision"
);
Now an old underwriting result cannot overwrite a newer one.
A better structure is:
struct CreditDecision {
uint256 limit;
uint64 expiresAt;
uint64 nonce;
bytes32 modelHash;
bytes32 policyHash;
}
and require:
new nonce > stored nonce
This turns the authorization state into a monotonic state machine.
24. Failure Handling
Now imagine the Base RPC provider goes offline.
Should credit applications immediately fail?
Not necessarily.
The ROFL application should distinguish between:
NO DATA
and:
LOW RISK
These are completely different states.
A safe policy is:
RPC unavailable
↓
no new underwriting decision
↓
existing authorization remains valid
until expiry
rather than:
RPC unavailable
↓
grant maximum credit
This is an example of fail-closed financial automation.
25. The Full Lifecycle
The final system now behaves like this:
sequenceDiagram
participant User
participant Base
participant ROFL
participant Sapphire
participant CreditApp
User->>Base: Deposit collateral
ROFL->>Base: Read collateral/activity
Base-->>ROFL: Public state
ROFL->>ROFL: Extract features
ROFL->>ROFL: Run private model
ROFL->>ROFL: Evaluate private policy
ROFL->>Sapphire: Submit authorization
Sapphire->>Sapphire: Verify ROFL origin
Sapphire->>Sapphire: Store confidential decision
CreditApp->>Sapphire: Can user draw X?
Sapphire-->>CreditApp: true / false
CreditApp->>User: Execute permitted action
Notice how little information crosses the boundary.
Base provides evidence.
ROFL produces a decision.
Sapphire enforces authorization.
The application receives only what it needs.
26. Testing the System
For the Solidity layer, Foundry is a good fit.
A basic test:
function testCreditExpires() public {
vm.warp(block.timestamp + 2 hours);
vm.expectRevert("expired");
credit.getCreditLimit(borrower);
}
Test stale updates:
function testRejectsStaleDecision() public {
credit.updateCredit(
borrower,
800,
40000,
uint64(block.timestamp + 1 days),
10
);
vm.expectRevert("stale decision");
credit.updateCredit(
borrower,
900,
50000,
uint64(block.timestamp + 1 days),
9
);
}
Test authorization:
function testOnlyROFLCanUpdate() public {
vm.prank(attacker);
vm.expectRevert("not authorized");
credit.updateCredit(
borrower,
900,
50000,
uint64(block.timestamp + 1 days)
);
}
These tests should exist before the ROFL worker is connected.
27. Testing the Risk Engine Independently
Do not make the TEE the only place where the model can be tested.
The scoring function should have deterministic unit tests.
describe("credit scoring", () => {
it("rewards repayment history", () => {
const score = calculateScore({
walletAge: 0.8,
activity: 0.7,
collateral: 0.9,
utilization: 0.2,
liquidations: 0,
repayment: 1,
volatility: 0.1
});
expect(score).toBeGreaterThan(700);
});
});
You want three separate testing layers:
model tests
+
contract tests
+
integration tests
Otherwise a failure becomes difficult to localize.
28. What If the ROFL Application Is Compromised?
This is where threat modeling becomes more interesting.
You should not assume:
"ROFL means the application can never be wrong."
Instead, design defense in depth.
For example:
ROFL
↓
credit limit
↓
Sapphire policy guard
↓
maximum protocol-wide limit
↓
application-level collateral check
↓
execution
Suppose ROFL accidentally returns:
$900,000
The Sapphire contract could enforce:
require(
limit <= protocolMaximum,
"limit exceeds protocol cap"
);
This gives us two independent layers:
off-chain underwriting
+
on-chain safety bounds
That is much stronger than putting every security assumption into one component.
29. Private Policy, Public Bounds
This leads to an especially useful pattern.
The detailed policy can remain confidential:
private model
private thresholds
private weights
private features
while broad safety bounds remain public:
maximum credit:
$50,000
maximum authorization:
24 hours
maximum utilization:
80%
Now users can understand the outer safety envelope without learning the exact underwriting algorithm.
This is often the right compromise between privacy and auditability.
30. What Can Be Publicly Audited?
A confidential application still needs auditability.
We can publish:
contract addresses
ROFL application identifier
model hash
policy hash
deployment version
authorization expiry
protocol-wide limits
upgrade events
revocation events
We don't need to publish:
every borrower score
private features
exact risk weights
individual underwriting inputs
This produces a useful distinction:
Public integrity
People can verify:
Which version is running?
Private computation
People cannot necessarily see:
What was the user's exact internal score?
Public safety envelope
People can still verify:
What is the maximum authority the system can grant?
That is a much more nuanced security model than simply calling the application "private."
31. Adding an Audit Manifest
I would also put a machine-readable manifest in the repository:
{
"protocol": "private-credit",
"model": {
"version": "credit-v1",
"hash": "0x..."
},
"policy": {
"version": "policy-v3",
"hash": "0x..."
},
"limits": {
"maximumCreditUsd": 50000,
"maximumAuthorizationSeconds": 86400
},
"execution": {
"runtime": "Oasis Sapphire",
"worker": "Oasis ROFL"
}
}
The manifest doesn't expose the policy.
It identifies it.
That gives you reproducibility without sacrificing confidential parameters.
32. Why This Is More Interesting Than a Private Database
A private database can hide information.
But it does not necessarily establish a decentralized authorization path.
Our design instead creates a chain:
public evidence
↓
confidential computation
↓
confidential authorization
↓
bounded execution
That is the interesting primitive.
The blockchain remains useful for:
- settlement
- collateral
- ownership
- authorization
- audit trails
while confidential infrastructure handles:
- private feature extraction
- sensitive models
- proprietary policy
- individual risk state
This is exactly the kind of workload for which combining Sapphire and ROFL becomes compelling.
33. Where This Architecture Could Be Used
Although we built a credit engine, the same architecture can support other systems.
Private insurance underwriting
Public collateral and claims data can feed a confidential underwriting engine.
Institutional DeFi
An institution can receive a private risk classification without publishing its entire portfolio.
Merchant financing
A merchant's transaction history can influence a credit line without exposing the complete scoring model.
Private derivatives
Risk limits can remain confidential while settlement remains on-chain.
DAO treasury controls
A confidential policy engine could evaluate risk before authorizing certain treasury actions.
Cross-chain credit
ROFL can collect evidence from multiple chains and aggregate it before producing a Sapphire authorization.
The last one is particularly interesting.
The public blockchains become data sources.
Sapphire becomes the confidential policy layer.
ROFL becomes the computation and connectivity layer.
34. Extending It to Multiple Chains
Suppose the borrower has assets on:
Ethereum
Base
Arbitrum
Optimism
ROFL can collect the relevant state from each network.
flowchart LR
ETH[Ethereum]
BASE[Base]
ARB[Arbitrum]
OP[Optimism]
ROFL[ROFL Risk Engine]
ETH --> ROFL
BASE --> ROFL
ARB --> ROFL
OP --> ROFL
ROFL --> S[Sapphire]
S --> CREDIT[Private Credit Controller]
Now the user does not need to manually consolidate the information.
The risk engine sees the portfolio as a whole.
Again, this doesn't make those chains private.
It means the aggregation and derived risk state can remain confidential.
35. The Important Trust Assumption
There is no magic here.
A responsible implementation must clearly document its assumptions.
Our confidentiality depends on the security properties of the trusted execution environment and the Oasis infrastructure supporting ROFL/Sapphire.
The application also has to ensure that its deployed binaries, configuration, key handling, authorization logic, and update process are correct.
ROFL's current architecture explicitly uses TEEs, remote attestation, application registration, and Oasis consensus as parts of this trust model.
Therefore, the correct claim is not:
"Nobody can ever know this information."
A better claim is:
"The protocol is designed so that sensitive computation and state are processed inside the confidential execution environment, subject to the documented TEE and protocol trust assumptions."
That is the statement I would put in the README.
36. Production Hardening Checklist
Before calling this production-ready, I would add:
- [ ] Sapphire-specific access-control primitives
- [ ] ROFL application authentication
- [ ] nonce/replay protection
- [ ] authorization expiry
- [ ] model versioning
- [ ] policy versioning
- [ ] policy hashes
- [ ] maximum protocol-wide credit limits
- [ ] stale-data detection
- [ ] RPC failover
- [ ] oracle freshness checks
- [ ] deterministic model tests
- [ ] Foundry contract tests
- [ ] integration tests
- [ ] deployment manifest
- [ ] upgrade controls
- [ ] emergency revocation
- [ ] monitoring
- [ ] attestation verification
- [ ] key rotation procedure
- [ ] incident-response procedure
The most important lesson is that confidentiality does not eliminate ordinary smart-contract security.
You still need:
access control
+
replay protection
+
bounds
+
expiry
+
monitoring
+
upgrade discipline
Privacy is another security property, not a replacement for the others.
37. The Bigger Design Pattern
The most useful takeaway from this project isn't the credit score.
It's the architecture.
We can think of a modern decentralized application as three different layers:
┌───────────────────────┐
│ PUBLIC WORLD │
│ │
│ Ethereum / Base / L2s │
│ settlement / assets │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ CONFIDENT COMPUTE │
│ │
│ ROFL TEE │
│ private algorithms │
│ external data │
│ sensitive processing │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ CONFIDENT ON-CHAIN │
│ │
│ Sapphire │
│ private state │
│ authorization │
│ policy enforcement │
└───────────────────────┘
The mistake is trying to force all three jobs into the same environment.
Public blockchains are excellent at settlement and transparent state.
Confidential runtimes are useful when state should not be globally observable.
TEE-backed off-chain computation is useful when workloads are too expensive, complex, or externally connected to execute directly on-chain.
ROFL and Sapphire allow those pieces to be composed.
38. Final Thoughts
The most interesting privacy applications aren't necessarily applications where everything is hidden.
They are applications where the confidentiality boundary is deliberate.
For our credit engine:
Collateral:
PUBLIC
Transactions:
PUBLIC
Settlement:
PUBLIC
Risk features:
CONFIDENTIAL
Underwriting model:
CONFIDENTIAL
Policy parameters:
CONFIDENTIAL
Credit decision:
CONFIDENTIAL
Safety limits:
PUBLIC
Model identity:
PUBLIC
Authorization status:
MINIMALLY DISCLOSED
That is a much more useful way to think about confidential blockchain applications.
Instead of asking:
"Can we put this entire application on a private chain?"
ask:
"Which information actually needs confidentiality?"
Then ask:
"Where should that information be processed?"
And finally:
"What is the smallest result that needs to cross the confidentiality boundary?"
In this architecture, Base provides the public evidence layer.
ROFL performs the expensive and sensitive underwriting computation.
Sapphire provides confidential state and policy enforcement.
The resulting system doesn't try to make public blockchains disappear.
It makes them more useful by adding a confidential computation layer where transparency would otherwise become a liability.
Repository Blueprint
The complete project can follow this structure:
private-credit/
├── contracts/
│ ├── PrivateCredit.sol
│ ├── CreditController.sol
│ ├── PolicyRegistry.sol
│ └── interfaces/
│ └── IPrivateCredit.sol
│
├── rofl/
│ ├── src/
│ │ ├── index.ts
│ │ ├── base.ts
│ │ ├── features.ts
│ │ ├── scoring.ts
│ │ └── sapphire.ts
│ ├── Dockerfile
│ └── package.json
│
├── scripts/
│ ├── deploy.ts
│ ├── seed.ts
│ └── request-underwriting.ts
│
├── test/
│ ├── PrivateCredit.t.sol
│ ├── CreditController.t.sol
│ └── integration.t.sol
│
├── frontend/
│
├── manifest/
│ └── protocol.json
│
├── foundry.toml
├── hardhat.config.ts
└── README.md
The official Oasis documentation provides the current Sapphire development documentation, ROFL workflow, and examples for integrating ROFL applications with confidential Sapphire contracts.
For developers wanting to experiment with the pattern, the official ROFL documentation also includes a trustless price-oracle example that demonstrates a ROFL application communicating with a confidential Sapphire contract.
Resources
Oasis Sapphire
https://docs.oasis.io/build/sapphire/
Develop on Sapphire
https://docs.oasis.io/build/sapphire/develop/
Oasis ROFL
https://docs.oasis.io/build/rofl/
ROFL Workflow and Architecture
https://docs.oasis.io/build/rofl/workflow/
Encrypted Events
https://docs.oasis.io/build/sapphire/develop/encrypted-events/
Oasis Build Documentation
https://docs.oasis.io/build/
The official documentation is the best place to verify the current APIs and deployment workflow because the Oasis tooling continues to evolve.
The core idea is simple:
Don't make the blockchain private.
Make the parts that shouldn't be public confidential and make the boundary between public evidence and private computation explicit.
That is where confidential Web3 infrastructure becomes much more interesting than simply hiding a smart contract's storage.
Top comments (0)