Confidentiality Pitfalls
OXN encrypts the contents of your contract state and calls. It does not automatically make everything about your dApp private. This page catalogs the ways real applications leak information despite the encryption.
Gas-based side channels
If your contract's Gas usage varies with the value of a secret, that variation is observable and leaks the value.
Example. A confidential ERC-20 that has a fee tier based on transfer amount:
function transfer(address to, uint256 amount) external {
// BAD: fee calculation gas depends on amount
uint256 fee;
if (amount > 1_000_000) { fee = amount / 100; } // ~2000 gas
else if (amount > 10_000) { fee = amount / 200; } // ~2200 gas
else { fee = amount / 500; } // ~1900 gas
// ...
}
An observer measures gasUsed and infers which branch executed, revealing the amount's approximate range.
Mitigation. Compute all branches, select at the end:
uint256 feeA = amount / 100;
uint256 feeB = amount / 200;
uint256 feeC = amount / 500;
uint256 fee = amount > 1_000_000 ? feeA : (amount > 10_000 ? feeB : feeC);
The ternary compiles to MSTORE + selection, which is constant-time. Not perfect (SLOAD counts still vary), but much closer.
Storage layout leaks
Every slot address is public. Which slots your contract writes and reads is visible to anyone dumping the storage tree.
Example. A private user-flag mapping:
mapping(address => bool) private hasClaimed;
function claim() external {
require(!hasClaimed[msg.sender], "already claimed");
hasClaimed[msg.sender] = true;
// ... reward user
}
The hasClaimed[user] slot is at keccak256(user . position). Its presence in the storage trie after a claim (regardless of encrypted value) reveals that user has claimed. An observer with access to raw storage dumps can enumerate who has claimed simply by checking which slots exist.
Mitigation. Either accept that "who called" is public (it's leaked by tx.from anyway), or use oblivious storage patterns (write to every user's slot on every call). Oblivious patterns are expensive; use them only when the leak matters.
Event count leaks
How many events a transaction emitted is public. Event data is encrypted, but their count is not — you can count logs in a receipt.
If your contract's logic emits N events for path A and M events for path B, the count reveals the path.
Mitigation. Emit the same number of events on every path — pad with no-op events if necessary.
Metadata analysis
Who calls what contract when is public. Even with all contents encrypted, the transaction graph shows:
- Sender and recipient addresses.
- Which contract was called.
- Block and timestamp.
- Gas paid.
If you're the only entity calling a specific contract at a specific time, that pattern reveals context regardless of encryption.
Mitigation. For scenarios where transaction-graph privacy matters, layer a mixer or relayer on top of OXN. The base confidentiality does not include this.
Timing side channels
Transaction inclusion time is public. A contract that behaves differently based on when it's called (e.g., "auction reveal is stricter after deadline") leaks the caller's intent through timing.
Mitigation. Same as above — timing is inherent to on-chain execution. If it matters, either don't put the sensitive logic on-chain, or accept that timing reveals coarse-grained information.
Compilation and code as an oracle
Contract bytecode is public. Anyone can read your Solidity (after verification) or decompile the bytecode. Your control flow, storage layout, and constant values are all visible.
Mitigation. Do not hide security-critical logic in the bytecode itself. Assume the bytecode is public and design for that.
Deployment history
Contract deployment transactions are public. The constructor arguments passed at deploy time are permanently visible in the deployment transaction's calldata. Never pass secrets as constructor arguments.
Store secrets via an encrypted post-deployment initialization call instead:
// Constructor: only public data
constructor(address _owner) { owner = _owner; }
// Post-deploy: secrets via encrypted call
function setSecret(bytes32 _secret) external {
require(msg.sender == owner);
secret = _secret; // calldata encrypted, storage encrypted
}
Cross-contract patterns
When a confidential contract calls another contract with plain arguments, those arguments become public. For example:
function pay(address recipient, uint256 amount) external {
// BAD if `amount` is meant to be private
token.transfer(recipient, amount); // amount visible in the sub-call
}
Depending on the target contract and how it's called, sub-call arguments may be encrypted (if it's a confidential contract) or plain (if it's a public one).
Mitigation. Understand whether the contract you're calling is confidential. Route calls through confidential contracts when arguments matter.
What to do about all this
- Threat-model your app. What actually needs to be private? Balances? Participants? Amounts? Vote choices?
- Design around the leak surface. For each thing you want to hide, ask "is the encryption sufficient, or does storage/gas/timing/metadata leak it?"
- Prefer confidential-friendly patterns. Approvals-based reads (see Access Control Patterns) are safer than public getters.
- Audit. A security audit that understands TEE-based confidentiality is worth the money.
Next steps
- Best Practices — the general checklist
- Common Vulnerabilities — standard EVM issues that still apply
- Confidentiality Model — the underlying encryption boundary