Confidential Smart Contracts on Base: A Deep Technical Look at Inco Lightning
Public blockchains made a radical trade. In exchange for trustlessness, they put everything on display — every balance, every transfer, every bid, every vote, forever. That transparency is the source of their integrity and, simultaneously, the single biggest barrier to a long list of real applications. You cannot run a sealed-bid auction where every bid is visible the instant it lands. You cannot pay salaries on rails where the whole org chart’s compensation is one block explorer query away. You cannot play poker if everyone can read your hand.
Inco is one of the more developer-pragmatic answers to that problem, and its first production system, Inco Lightning, is live on Base. This is a technical walkthrough of how it works, why it is a genuinely good privacy primitive for Base specifically, and why a Solidity developer can adopt it in an afternoon. It also tries to be honest about the trust assumptions, because a privacy system you don’t understand is a privacy system you can’t trust.
Confidentiality is not anonymity
The first thing to get straight is the threat model, because Inco solves a narrower and more useful problem than “privacy” in the abstract.
Inco delivers confidentiality, not anonymity. Your identity — the address signing the transaction — stays public. What gets hidden is the data: the amount, the balance, the bid, the boolean. This distinction is not a limitation to apologize for; it is a deliberate fit for the use cases that actually need to ship. Payroll, supplier payments, treasury operations, and most institutional flows are situations where the parties are known to each other (and sometimes legally must be) but the figures should not be broadcast to competitors. A model where addresses are public but amounts are hidden is also far friendlier to compliance and risk screening than full anonymity, which is precisely why confidentiality, rather than mixing, is the more enterprise-tractable design.
The second thing to get straight: Inco is not a new chain. It is a confidentiality layer that integrates with chains you already use. The team’s own analogy is TLS/SSL for the internet — a layer you opt into for the parts of your application that need it, without re-architecting everything beneath it. You write standard Solidity, deploy to Base, and reach for encrypted types only where confidentiality earns its keep.
The core mental model: handles and symbolic execution
If you internalize one idea from this article, make it this one. Everything else follows.
When a contract holds an encrypted value, it does not hold the ciphertext on-chain. It holds a handle — a unique, immutable identifier pointing to a piece of hidden data that lives off-chain. The encrypted types are literally defined as 32-byte identifiers:
type euint256 is bytes32;type ebool is bytes32;type eaddress is bytes32;So euint256, ebool, and eaddress are the hidden counterparts of uint256, bool, and address. If you call balanceOf on a confidential token and inspect the raw return value on a block explorer, you get something like: 0xa8d84064218bfc979af10dccc8153c9ab8a15068c3d64cb63927aca8ad1a3c9c
That tells an observer nothing about the actual balance. It is a pointer, not a number.
This leads directly to the model Inco calls symbolic execution. On-chain, your contract manipulates handles. When you write e.add(a, b), the chain does not add two numbers — it records that a new handle should represent “the sum of whatever a and b point to,” emits an event, and hands you back a fresh handle immediately. You can chain operations on that result in the same transaction without waiting for any confirmation. The actual arithmetic over the actual plaintext happens asynchronously, off-chain, inside a secure environment, after the transaction is included. The on-chain program is a symbolic recipe; the real computation is reproduced over real values elsewhere.
Two consequences worth holding onto:
- Handles are immutable and never deleted. Reassigning
balanceOf[msg.sender] = balanceOf[msg.sender].sub(value)does not mutate the old encrypted value — it creates a new handle for the new balance. The old handle still exists and still corresponds to the old encrypted value, whether or not your contract keeps a reference to it. From Inco’s standpoint, handles are created but never destroyed. This matters for reasoning about what a value’s history exposes. - The chain is the source of truth for structure; the off-chain layer is the source of truth for values. This separation is the whole trick that lets confidential computation feel like ordinary Solidity.
Architecture: three moving parts
Inco’s system has three components that cooperate to make the handle model work.
1. The smart contract library (@inco/lightning). This is the on-chain piece. It extends the EVM — without modifying it — with the encrypted types, the arithmetic and comparison operations (e.add, e.sub, e.mul, e.div, e.eq, e.lt, e.select, and so on), and the access-control primitives (e.allow). Under the hood, every operation is a call into a singleton Inco contract that checks access-control rules and emits an event describing the operation. Those events are the symbolic recipe the off-chain layer follows.
2. The Confidential Compute Server, running inside a TEE. This is where the real work happens. Inco Lightning’s confidential compute executes inside a Trusted Execution Environment — a hardware-isolated secure enclave on the CPU where even the node operator cannot observe what is being processed. The server watches the chain for the events emitted by the library, performs the actual encrypted computation and any authorized decryption inside the enclave, validates access control before it ever decrypts anything, and returns results with cryptographic attestations. Notably, it monitors the chain and returns results directly, without requiring a separate callback transaction for every operation — which keeps the on-chain footprint and latency low.
3. The client-side JavaScript library (@inco/lightning-js). This is the user’s side. It encrypts inputs against the network’s public key before they are ever sent on-chain, generates the ephemeral keys and EIP-712 signatures needed to authorize a decryption, and decrypts results locally for the user. The plaintext a user is entitled to see is reconstructed in their browser, not exposed on a server.
Putting it together: a user encrypts an input client-side, submits it in a normal Base transaction, the contract turns it into a handle and records symbolic operations, the TEE reproduces those operations over the real ciphertext, and authorized parties pull attested results back out. To the user it looks like a slightly-slower-to-finalize dApp. To the chain it looks like a contract shuffling 32-byte blobs.
Encrypted types and operations: it reads like Solidity
The reason adoption is fast is that the API is deliberately boring. Here is a complete, working confidential ERC-20-style token taken from the concepts guide — read it as ordinary Solidity with encrypted types swapped in:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
import {euint256, ebool, e} from "@inco/lightning/src/Lib.sol";
contract SimpleConfidentialToken {
using e for *;
mapping(address => euint256) public balanceOf;
constructor() {
balanceOf[msg.sender] = uint256(1000 * 1e9).asEuint256();
}
function transfer(address to, bytes memory valueInput) external returns (ebool) {
euint256 value = valueInput.newEuint256(msg.sender);
return _transfer(to, value);
}
function transfer(address to, euint256 value) public returns (ebool success) {
require(
msg.sender.isAllowed(value),
"SimpleConfidentialToken: unauthorized value handle access"
);
return _transfer(to, value);
}
function _transfer(address to, euint256 value) internal returns (ebool success) {
success = balanceOf[msg.sender].ge(value);
euint256 transferredValue = success.select(value, uint256(0).asEuint256());
euint256 senderNewBalance = balanceOf[msg.sender].sub(transferredValue);
euint256 receiverNewBalance = balanceOf[to].add(transferredValue);
balanceOf[msg.sender] = senderNewBalance;
balanceOf[to] = receiverNewBalance;
senderNewBalance.allow(msg.sender);
receiverNewBalance.allow(to);
senderNewBalance.allowThis();
receiverNewBalance.allowThis();
}
}
The full operation set covers what you’d expect: add, sub, mul, div, rem, the bitwise and shift/rotate family, comparisons (eq, ne, ge, gt, le, lt, min, max, not), and on-chain confidential randomness via e.rand() and e.randBounded(). Comparisons return an ebool; arithmetic returns an euint256. You can mix encrypted and plaintext operands freely — a.add(5) is fine.
The one pattern you must learn: no branching on secrets
There is exactly one place where confidential Solidity stops looking like normal Solidity, and it’s a consequence of physics, not API design. You cannot branch on an encrypted value, and you cannot revert based on one. If your if/else took a different code path depending on a hidden number, the path itself would leak the number — an observer would learn the secret by watching which branch executed. The same logic rules out reverting on a secret condition.
The replacement is the multiplexer pattern via select, which is a data-flow conditional rather than a control-flow one:
// Instead of: if (balance >= value) { transfer } else { revert }
success = balanceOf[msg.sender].ge(value); // ebool
euint256 transferredValue = success.select(value, uint256(0).asEuint256());
If the sender has enough balance, transferredValue becomes value; otherwise it becomes an encrypted zero, and the rest of the transfer arithmetic runs unchanged. An insufficient balance silently transfers nothing instead of reverting — which is exactly the point, since a revert would itself leak the balance comparison. Expect this “compute both branches, select the right one blindly” shape throughout confidential code. It takes a transaction or two to feel natural, then becomes second nature.
Inputs, context binding, and fees
External values enter the system as ciphertexts. The contract converts them into handles with newEuint256 (and newEbool, newEaddress):
function transfer(address to, bytes memory valueInput) external payable returns (ebool) {
require(msg.value >= inco.getFee() * 1, "Fee Not Paid");
euint256 value = valueInput.newEuint256(msg.sender);
// ...
}
newEuint256 takes the encrypted bytes and the address performing the input — which must be the account that actually created the ciphertext, because that account is granted decryption rights over the resulting handle. Passing anyone else’s address would be a security bug.
A subtle and important detail: the client SDK binds context into every ciphertext — the originating account, the chain, and the target contract. If someone tries to replay another user’s ciphertext to steal decryption access, the context won’t match and the handle silently falls back to its default value (0 for euint256, false for ebool). Malformed input degrades to the default rather than reverting, so contracts need to account for that rather than assume every input is well-formed.
On the economics: fees are charged only where off-chain work is unavoidable. Creating a handle from an external ciphertext or generating randomness costs a fee (the docs example shows inco.getFee() returning 0.0001 ETH), because those operations require the TEE to ingest and process ciphertext. Everything symbolic — every add, sub, comparison, select, trivial encrypt (asEuint256), and every access-control call — is free, because it’s just events on-chain that the TEE reproduces later. You count one fee per newE* or rand* call:
require(msg.value >= inco.getFee() * ciphertextCount, "Fee Not Paid");
Fees can be paid by the user via msg.value, or absorbed by the contract from its own ETH balance so end users never have to think about them — a meaningful UX lever for consumer apps. Always read inco.getFee() dynamically rather than hardcoding, since it can change.
Programmable access control: privacy you can reason about
This is the part that makes Inco a privacy system rather than just an encryption system, and it’s where the design is strongest.
Access control over encrypted data is fully programmable and fully on-chain. Who is allowed to decrypt a given ciphertext is itself public information, visible on-chain, even though the value is not. You grant access with e.allow:
senderNewBalance.allow(msg.sender); // let the sender see their new balance
receiverNewBalance.allow(to); // let the receiver see theirs
senderNewBalance.allowThis(); // let THIS contract compute over it later
receiverNewBalance.allowThis();
allowThis() is sugar for allow(value, address(this)). Because handles are immutable, granting access shares only the current value — update the variable and you must grant access to the new handle. The single most common beginner bug is forgetting to call allowThis() after updating a stored encrypted variable, which leaves the contract unable to compute over it in any future transaction.
There’s also a transient allowance concept: the result of an operation like e.add is transiently allowed to the calling contract for the duration of that transaction, which is why you can chain operations back-to-back. But transient access evaporates at the end of the transaction — hence the need for explicit allowThis() to persist compute rights.
The right way to reason about access is blunt and worth quoting in spirit: assume any account that ever received access to a ciphertext — transiently or permanently — now knows the value, has stored it, and may re-share it or publicly decrypt it. A transient allowance is not “safer” than a permanent one in any meaningful sense. Access control is a capability you hand out, not a leash you keep. Designing a confidential app is largely the discipline of being deliberate about exactly who you allow, and when.
Decryption and attestations: getting plaintext out, verifiably
Encryption is only half the story; eventually authorized parties need plaintext, and sometimes the chain needs to act on it. Inco offers three attested flows, all gated by on-chain access control and all proven with EIP-712 signatures.
The signature scheme binds three things together — the caller’s address, the requested operation, and the specific handles involved — so that only an address with genuine on-chain access (granted via e.allow or e.reveal) can request an attestation, and so requests cannot be forged or replayed.
Attested Decrypt. An authorized user asks the network to decrypt a handle and return the plaintext plus a signed attestation. The network checks the ACL contract, and only if the caller is permitted does each node decrypt and sign. The user can then post that attestation back on-chain, where a contract verifies the signatures and uses the value. Useful when a decrypted result must re-enter contract logic with proof of correctness.
Attested Compute. This is the elegant one. Instead of revealing a raw secret, you evaluate a predicate over it off-chain and get back only the answer. Want to prove creditScore >= 700 without exposing the score? Attested Compute runs the comparison in the enclave and returns an ebool result with a signature, so the decision can be verified on-chain without anyone — including the chain — learning the underlying number. This is selective disclosure done right: reveal the conclusion, not the evidence.
Attested Reveal. When a contract calls e.reveal(handle), the value is made public deliberately, and from then on anyone can request an attested decryption of it. The warning is sharp and permanent: once e.reveal runs, that ciphertext is public forever. Reveal is for end-states — the winning bid after an auction closes, the final board after a game ends — never for intermediate secrets.
When you accept any attested result on-chain, there is a non-negotiable safety check: verify the attestation corresponds to the handle you actually expect, not just that the signature is valid. Otherwise an attacker can submit a perfectly valid attestation for a different handle they swapped in:
function submitDecryption(
DecryptionAttestation memory decryption,
bytes[] memory signatures
) external {
require(
inco.incoVerifier().isValidDecryptionAttestation(decryption, signatures),
"Invalid Signature"
);
// Signature validity is NOT enough — bind it to the intended handle:
require(euint256.unwrap(randomNumber) == decryption.handle, "Handle mismatch");
decryptedNumber = uint256(decryption.value);
}
Why this is good privacy on Base specifically
Plenty of privacy schemes exist on paper. What makes Inco Lightning a practical fit for Base comes down to a handful of properties.
It runs at the speed of Base, not a side-chain’s speed. Because confidential operations execute as symbolic events on the host chain with the heavy lifting offloaded to the TEE, Inco processes transactions at close to native chain speed with very low added latency. There is no separate privacy chain to bridge to, no second mempool, no alternative finality to wait on. Your confidential token and your public DeFi protocol live on the same Base, composable with the same tooling.
The operator cannot steal, and users can force-exit. The network advertises end-to-end verifiability of every state transition — meaning the off-chain operator’s work is checkable against on-chain commitments, which is what makes operator theft infeasible rather than merely discouraged. Users retain control of funds with a force-exit path, the standard “the operator can’t hold you hostage” guarantee that any serious layer needs.
It targets post-quantum security at the encryption layer. Inco’s public materials describe a NIST-approved post-quantum (PQS) encryption scheme for the confidentiality layer. With NIST having finalized its post-quantum standards (ML-KEM/FIPS 203, ML-DSA/FIPS 204, and SLH-DSA/FIPS 205) and regulators in the US and EU now pushing migration timelines, building the encryption of hidden state on a quantum-resistant scheme is a forward-looking choice — particularly because confidential on-chain data is a “harvest now, decrypt later” target: anything encrypted today and stored on an immutable ledger must stay secret for decades. The honest caveat is that this protects Inco’s confidentiality layer; the underlying Base chain still relies on classical ECDSA for its own signatures, so this is post-quantum confidentiality, not a fully post-quantum stack end to end. For the data Inco is responsible for hiding, though, it’s the right posture.
The confidentiality model fits real Base use cases. Shielded ERC-20 balances and transfers; private DeFi primitives like blind auctions, dark-pool-style order flow, DvP escrow, and private governance; provably fair games with sealed bids and hidden state; and consumer payments where you hide the amount but not the counterparty. These are applications that are either impossible or badly compromised on a transparent chain, and they map cleanly onto “hide the value, keep the identity.”
Why it’s genuinely easy to develop on
The privacy story would be academic if the developer experience were painful. It isn’t.
Standard Solidity, one import. No new language, no new chain, no exotic toolchain. You add import {euint256, ebool, e} from "@inco/lightning/src/Lib.sol"; to a normal contract and you’re writing confidential logic. The token contract above is the entire surface area you need to understand to ship a confidential token.
Your existing tools work. Foundry and Hardhat are both first-class, with official templates (lightning-rod for Foundry, inco-lite-template for Hardhat). The one-minute quickstart is a git clone, bun install, bun run test. Scaffolding a full confidential dApp is a single npx create-inco-app.
You can test confidential contracts in pure Solidity. This is underrated. Inco ships Foundry cheatcodes that fully simulate the confidential environment — including decryption — inside your Solidity tests, so you can assert against plaintext values in a local test without standing up the whole network. A local covalidator runs via Docker for integration testing. Testing privacy logic is usually where these systems get awful; here it’s a normal forge test loop.
The client side is handled. @inco/lightning-js covers encrypting values, generating the EIP-712 signatures for decryption, and the encrypt → transact → reveal/decrypt → render loop. There’s a Next.js starter, and out-of-the-box wallet integrations for Privy, Dynamic, RainbowKit, Reown (AppKit), and Para — so the auth layer most dApps spend days wiring up is documented and ready.
There’s a first-class AI build path. Inco publishes an agent skill installable into Claude Code, Codex, Cursor, and 70+ agents via npx skills add Inco-fhevm/skills. It does more than autocomplete — it encodes a decision tree for what should be private and when it reveals, a catalog of confidential-game archetypes (encrypted board, fog-of-war, hidden hand, hidden roles, sealed-bid auction, simultaneous-move, provably-fair casino, word-guessing), the two settlement models (on-chain attestation for wagers vs. private client-side decrypt for single-player), and full worked contracts. For a developer who knows Solidity but is new to confidential design, that “game-design sense” layer is the actual hard part, packaged.
Taken together, the on-ramp is: clone a template, write Solidity you already know with three new types, test it in forge, and wire a documented frontend. That’s the claim of “build a confidential app in 20 minutes,” and it’s largely defensible.
Limits of Trust Assumptions and Limits
A deep look owes you the trade-offs, because they determine whether Lightning is right for your app.
Inco Lightning’s confidentiality rests on a TEE, and TEEs are a hardware trust assumption. Secure enclaves like the ones Lightning uses are fast and practical, but their security depends on trusting the hardware vendor’s implementation and supply chain, and the category has a long history of side-channel attacks. This is a real and different trust model from pure cryptography. Inco’s own framing is candid about this trade-off, which is why the team is also building Inco Atlas, an FHE- and MPC-based engine for the highest-stakes scenarios — elections, institutional finance, sensitive identity — where you want privacy that doesn’t depend on trusting hardware, at the cost of more latency. Lightning is the fast, pragmatic tier; Atlas is the maximal-assurance tier. Knowing which one a given application needs is part of designing responsibly, and for a great many consumer and DeFi use cases, the TEE model is an entirely reasonable choice.
The hardest bugs are at the application layer, not the crypto layer. The cryptography can be flawless while your dApp leaks everything through information leakage — secrets an observer deduces from public side effects. Naively porting an AMM with public pool prices but confidential swap amounts leaks the amounts, because the price moves before and after the swap give them away. A sealed auction that continuously publishes the current highest bidder lets an attacker binary-search the highest bid by submitting escalating bids until they win. These are not Inco bugs; they’re design bugs that confidential infrastructure makes possible to avoid but doesn’t avoid for you. Thinking adversarially about “what can be inferred from what’s still public” is the core skill.
Capability discipline is on you. Access, once granted, can be re-shared or revealed by the grantee. Session vouchers, used for smooth UX, grant the holder access to all of the signer’s encrypted handles across every Inco dApp, not just yours — so keep their lifetimes in minutes, revoke promptly, never store or forward them, and never suppress the wallet warning users sign. delegatecall into a contract that holds your ciphertexts can decrypt or re-share them. And because handles are never deleted, old values persist; reason about what a value’s lineage exposes.
It’s young infrastructure. Inco Lightning moved from Base Sepolia testnet (April 2025) onto Base mainnet, and the project is backed by a strong roster — Coinbase Ventures, Circle Ventures, and a16z’s Crypto Startup School among them — but this is early-stage technology, and you should weigh audit status and maturity for anything holding serious value. Treat it as a powerful new primitive, not a battle-hardened one.
Where it Lands
Inco Lightning is a well-judged piece of engineering. The handle-and-symbolic-execution model is the right abstraction for bolting confidentiality onto a transparent EVM chain without forking it, the programmable on-chain ACL turns “encrypted data” into “privacy you can actually reason about and verify,” and the attestation flows — especially Attested Compute’s reveal-the-predicate-not-the-value pattern — are exactly what selective disclosure should look like on-chain. The developer experience is the part that will drive adoption: standard Solidity, real testing in Foundry, ready-made frontends, and an AI skill that packages the genuinely hard confidential-design decisions.
The trade-off you’re accepting in the Lightning tier is a hardware trust assumption in exchange for native-chain speed, with Atlas waiting in the wings for cases that can’t accept that assumption. For a developer on Base who wants to ship a shielded token, a sealed-bid auction, a private payment app, or a game with hidden state — and wants to do it this week, in a language they already know — that is a very reasonable bargain.
The block explorer wasn’t supposed to be able to read your salary, your hand, or your bid. On Base, with Inco, it no longer can.
Resources: Inco docs · Quickstart · Confidential token tutorial · Build with AI · GitHub
