Before We Begin: What Problem Are We Solving?
Imagine you have a regular piggy bank sitting on your desk. You know how much money is inside. Nobody else does, because they cannot see through the ceramic.
Now imagine putting that piggy bank on a giant public billboard in the middle of a city. Suddenly, everyone walking past can see exactly how much you have saved. That is exactly what happens when you store financial data on a public blockchain like Ethereum. Every single number, deposit, balance, and transactions becomes visible to anyone in the world, forever.
This is not just an inconvenience. It is a real problem for real people:
- A business cannot hide its revenue from competitors
- A person cannot shield their savings from bad actors
- A protocol cannot run fair auctions without participants gaming the system
For years, blockchain developers worked around this by keeping money off-chain or using complicated workarounds. But none of those solutions felt natural. They all came with tradeoffs.
FHE changes that completely.
What Is FHE and Why Does It Matter?
FHE stands for Fully Homomorphic Encryption. That is a big phrase, so let's break it down with a simple story.
Picture a locked glass box. You put a number inside and lock it. You hand the box to a stranger. The stranger can do math on the number inside, they can add to it, subtract from it, compare it and all without ever opening the box. When you finally unlock it, the result of all that math is sitting there, correct, and the stranger never learned what the original number was.
That is FHE. It is a type of encryption that lets computers perform calculations on data while the data is still encrypted. The computer never sees the actual value. It only works with the scrambled, locked version.
This makes something previously impossible now completely possible: a smart contract that does math on private data without ever knowing what that data actually is.
What Is Fhenix CoFHE?
Fhenix built a system called CoFHE (Co-processor for Fully Homomorphic Encryption). Think of it as a powerful behind-the-scenes assistant that handles all the heavy FHE operations for your smart contract.
When your smart contract needs to add two encrypted numbers, it does not do it alone. It asks the CoFHE network to perform the computation. The CoFHE network does the work, returns the encrypted result, and nobody, not the network, not the blockchain, not any observer ever sees the actual numbers involved.
This means you can write smart contracts that handle private balances, private votes, private bids, or any kind of private data, and it all runs on the public Ethereum blockchain without exposing anything.
What Is the PiggyBank Contract?
The PiggyBank contract is a savings account that lives on the blockchain. Each user can:
- Deposit an amount into their personal piggy bank
- Withdraw an amount from their piggy bank
- Check their balance — privately, only the user can read it
What makes it special: every single number involved, the deposit amount, the withdrawal amount, the balance, is always encrypted on-chain. Even the miners processing the transactions, the validators running the network, and anyone staring at the blockchain data cannot tell how much money you have or how much you moved.
Only you, using your private wallet key, can decrypt and read your own balance.
Here is the complete contract example before we walk through the entire contract, line by line.
Piggy Bank Complete Contract Example
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;
import "@fhenixprotocol/cofhe-contracts/FHE.sol";
contract PiggyBank {
mapping(address => euint64) private balances;
mapping(address => bool) private initialized;
function _ensureInitialized() internal {
if (!initialized[msg.sender]) {
balances[msg.sender] = FHE.asEuint64(0);
FHE.allowThis(balances[msg.sender]);
FHE.allow(balances[msg.sender], msg.sender);
initialized[msg.sender] = true;
}
}
function deposit(InEuint64 memory encryptedAmount) public {
_ensureInitialized();
euint64 amount = FHE.asEuint64(encryptedAmount);
balances[msg.sender] = FHE.add(balances[msg.sender], amount);
FHE.allowThis(balances[msg.sender]);
FHE.allow(balances[msg.sender], msg.sender);
}
function withdraw(InEuint64 memory encryptedAmount) public {
_ensureInitialized();
euint64 amount = FHE.asEuint64(encryptedAmount);
ebool hasSufficient = FHE.gte(balances[msg.sender], amount);
euint64 newBalance = FHE.select(
hasSufficient,
FHE.sub(balances[msg.sender], amount),
balances[msg.sender]
);
balances[msg.sender] = newBalance;
FHE.allowThis(balances[msg.sender]);
FHE.allow(balances[msg.sender], msg.sender);
}
function getBalance() public view returns (euint64) {
return balances[msg.sender];
}
function allowBalancePublicly() public {
FHE.allowPublic(balances[msg.sender]);
}
function revealBalance(uint64 plaintext, bytes memory signature) public {
FHE.publishDecryptResult(balances[msg.sender], plaintext, signature);
}
function getDecryptedBalance() external view returns (uint256) {
(uint256 value, bool decrypted) = FHE.getDecryptResultSafe(balances[msg.sender]);
if (!decrypted) revert("Balance is not ready");
return value;
}
}
The Full Contract, Explained Line by Line
// SPDX-License-Identifier: UNLICENSED
This is a legal label. It tells anyone reading the code what the license is.
pragma solidity ^0.8.25;
This tells the Solidity compiler which version of the Solidity language to use when turning this code into something the blockchain can run.
import "@fhenixprotocol/cofhe-contracts/FHE.sol";
This line brings in Fhenix's FHE toolkit. It is like importing a library of special tools. Without this line, the contract would have no idea what encrypted numbers are or how to do math on them. Everything prefixed with FHE. in the rest of the contract comes from this import.
contract PiggyBank {
This declares the smart contract itself. The word contract in Solidity is like the word class in other programming languages.
The Storage: Where Data Lives
mapping(address => euint64) private balances;
This is where every user's balance is stored. Let's unpack it:
- A (
mapping) is like a dictionary or a lookup table. You give it a key and it gives you a value. Here, the key is a wallet address and the value is a balance. - The (
euint64) is the encrypted version of a 64-bit integer. A regular (uint64) would be a plain number visible to everyone. The (e) prefix means it is encrypted. It is a locked version of the number that only FHE operations can work with. - The (
private) keyword means this storage slot cannot be read by other contracts.
So this line creates a lookup table where: (wallet address → encrypted balance).
mapping(address => bool) private initialized;
This is a simple yes/no tracker. It records whether a user has ever interacted with the contract before. The (bool) means it is either (true) or (false).
✍️ This exists because FHE math requires a valid encrypted number to start with. You cannot add something to nothing, you need an encrypted zero as a starting point. This flag keeps track of whether that starting point has been created for each user yet.
Function 1: _ensureInitialized — Setting Up a New User's Account
function _ensureInitialized() internal {
This is a helper function. The underscore at the start is a naming convention that signals to other developers that this is an internal utility and it's not meant to be called directly from outside the contract.
if (!initialized[msg.sender]) {
The (msg.sender) is automatically filled in by the blockchain. It is the wallet address of whoever is currently calling the contract. The (!) means "not." So this reads as: "If this user has NOT been initialized yet, do the following..."
balances[msg.sender] = FHE.asEuint64(0);
This creates an encrypted zero and stores it as the user's starting balance. The (FHE.asEuint64(0)) takes the plain number zero and converts it into an encrypted form. This is the blank canvas that all future FHE math will build upon.
FHE.allowThis(balances[msg.sender]);
Encrypted numbers in CoFHE have a permission system. Just because a number is stored in this contract does not automatically mean the contract can use it for further calculations.
This line gives the PiggyBank contract itself permission to work with this encrypted balance in future operations. Without this, the very next deposit would fail because the contract would not be allowed to touch the balance it just created.
FHE.allow(balances[msg.sender], msg.sender);
This gives the user permission to decrypt and read their own balance. Without this line, even the owner of the money would not be able to see how much they have.
initialized[msg.sender] = true;
}
}
Mark this user as set up. Next time they call (deposit) or (withdraw), the (if) check at the top will skip all of this because (initialized[msg.sender]) will already be set to (true).
Function 2: deposit — Putting Money In
function deposit(InEuint64 memory encryptedAmount) public {
This is the deposit function. Let's look at the input:
- The (
InEuint64) is a special type that represents an encrypted number coming in from outside the contract i.e., from the user's wallet, via the CoFHE SDK. - The (
memory) keyword means this data is temporarily held in memory while the function runs, not permanently stored on the blockchain in this raw form. - The (
public) public means anyone — any wallet address on the internet — is allowed to call this function.
_ensureInitialized();
Before doing anything else, make sure the user has an encrypted zero balance set up. If they are a first-time user, this creates it. If they have deposited before, then it does nothing.
euint64 amount = FHE.asEuint64(encryptedAmount);
The incoming encrypted amount (InEuint64) needs to be converted into the internal encrypted format (euint64) that FHE operations understand.
balances[msg.sender] = FHE.add(balances[msg.sender], amount);
This is the heart of the deposit. (FHE.add) adds two encrypted numbers together and returns an encrypted result.
FHE.allowThis(balances[msg.sender]);
FHE.allow(balances[msg.sender], msg.sender);
}
After updating the balance, permissions must be re-applied to the new encrypted value.
Every time an FHE operation produces a new encrypted result, that result is a brand new locked box. The old permissions do not carry over automatically.
So the contract re-grants itself and the user access to this newly produced balance, exactly like it did during initialization.
Function 3: withdraw — Taking Money Out
function withdraw(InEuint64 memory encryptedAmount) public {
_ensureInitialized();
euint64 amount = FHE.asEuint64(encryptedAmount);
✍️ The opening lines are identical to (
deposit). Same idea: accept an encrypted amount from outside, make sure the user is set up, convert the amount into the internal format.
ebool hasSufficient = FHE.gte(balances[msg.sender], amount);
This line asks a question: "Is the current balance greater than or equal to the withdrawal amount?" But it asks that question without revealing either number.
- The (
FHE.gte) stands for "greater than or equal to." It compares two encrypted numbers and returns an encrypted boolean — a (ebool). An (ebool) is an encrypted yes/no answer. Neither the contract nor any observer knows whether the answer is yes or no. It is a locked answer inside a locked box.
This is remarkable. Normally, to check if you have enough money, a system would have to look at your balance. Here, the system performs the check without ever seeing the balance.
euint64 newBalance = FHE.select(
hasSufficient,
FHE.sub(balances[msg.sender], amount),
balances[msg.sender]
);
This is the most clever part of the entire contract. The (FHE.select) is like an encrypted if/else statement. It takes three arguments:
- An encrypted condition (
hasSufficient— the yes/no result from above) - The value to use i_f the condition is yes_ — (
FHE.sub(balances[msg.sender], amount)), which is the balance minus the withdrawal - The value to use if the condition is no — (
balances[msg.sender]), the unchanged balance
However, (FHE.select) picks one of the two options based on the encrypted condition, all while everything remains locked.
The contract does not know which branch was taken. A blockchain observer does not know which branch was taken.
Nobody knows whether the withdrawal actually went through or was silently rejected due to insufficient funds.
✍️ This is intentional. If the contract said "error: insufficient funds," that itself would reveal information and it would tell the world that your balance is less than the amount you tried to withdraw. FHE prevents even that leak.
balances[msg.sender] = newBalance;
FHE.allowThis(balances[msg.sender]);
FHE.allow(balances[msg.sender], msg.sender);
}
Store the new balance (whatever it ended up being — either reduced or unchanged) and re-apply permissions to the new encrypted value. Same pattern as in (deposit).
Function 4: getBalance — Reading Your Encrypted Balance
function getBalance() public view returns (euint64) {
return balances[msg.sender];
}
This returns your encrypted balance. A few things to note:
The (
view) keyword means this function does not change anything on the blockchain. It only reads. Because of this, it costs no gas to call.It returns (
euint64) — the encrypted version of the number. If you look at this return value directly, you will see a large scrambled number, not your actual balance.The actual decryption happens off-chain, in your own machine, using your wallet's private key and the CoFHE SDK.
✍️ This separation is important: the encrypted value travels through the public blockchain, but the decryption only happens on your private machine. Nobody else can perform that decryption because they do not have your private key.
Function 5: allowBalancePublicly — Step 1 of On-Chain Decryption
function allowBalancePublicly() public {
FHE.allowPublic(balances[msg.sender]);
}
This function is the first step in a three-step process for revealing your balance on-chain — meaning that the decrypted number is written back to the blockchain where anyone can see it.
Why would you want this? Imagine a lending protocol that needs to verify you have at least a certain balance before giving you a loan. The lender cannot run your private decryption for you. You need a way to prove your balance on-chain in a trustworthy way.
Function 6: revealBalance — Step 3 of On-Chain Decryption
function revealBalance(uint64 plaintext, bytes memory signature) public {
FHE.publishDecryptResult(balances[msg.sender], plaintext, signature);
}
After the Threshold Network decrypts the balance (Step 2, which happens automatically off-chain), it produces two things:
- The plaintext — the actual number, in the open
- A signature — a cryptographic stamp of approval proving the Threshold Network computed this result honestly and did not make it up
This function takes both and publishes them on-chain. The signature is the key part, which means you cannot submit a fake number. Only the legitimate result, produced by the real Threshold Network, will be accepted.
Function 7: getDecryptedBalance — Reading the On-Chain Result
function getDecryptedBalance() external view returns (uint256) {
(uint256 value, bool decrypted) = FHE.getDecryptResultSafe(balances[msg.sender]);
if (!decrypted) revert("Balance is not ready");
return value;
}
This is the final reading step. The (
external) keyword means only wallets and other contracts outside ofPiggyBankcan call this, notPiggyBankitself.The (
FHE.getDecryptResultSafe) checks whether the on-chain decryption result exists yet. It returns two values:(
value) — the decrypted number (if it exists)(
decrypted) — a true/false flag indicating whether the decryption has been completed
If someone calls (getDecryptedBalance) before going through the full three-step process, the function immediately stops and returns the message "Balance is not ready." This prevents people from accidentally reading a default zero and thinking their balance is empty.
Real-World Use Cases This Contract Unlocks
Private Savings Accounts — Exactly what this contract is. People can save money on-chain without revealing their wealth to the world.
Confidential Payroll — A company could pay employees through smart contracts without every salary being visible to colleagues and competitors.
Sealed Auction Bids — An auction where nobody can see what others have bid until the auction closes, preventing last-second sniping.
Private Lending — Borrow against a balance without revealing how much collateral you have, protecting you from targeted attacks.
Healthcare Data — Store sensitive health records on-chain, allowing computations (like insurance eligibility checks) without revealing the underlying medical data.
Confidential Voting — Run on-chain elections where votes are tallied correctly but no single vote is ever publicly linked to a voter.
Embrace the Future of Onchain Privacy
We are at the beginning of a new chapter in blockchain development. For years, developers accepted a painful tradeoff: either use a blockchain (transparent, trustless, permanent) or keep your data private (off-chain, centralized, trust-dependent). You could not have both.
FHE breaks that tradeoff. You can now have a public, trustless, censorship-resistant blockchain and keep your users' data genuinely private at the same time. Not hidden behind a terms-of-service agreement. Not protected by a promise from a company. Mathematically private, enforced by cryptography.
Fhenix CoFHE makes this available today, not as a research experiment, not as a future roadmap item, but as a working system you can build on right now, on Ethereum testnets, with familiar Solidity code.
The barrier to entry is low. If you know how to write a regular Solidity contract, you already know 90% of what you need to write a confidential one. You replace (uint64) with (euint64). You use (FHE.add) instead of (+). You use (FHE.select) instead of (if/else). The concepts translate directly.
What you get in return is something no regular smart contract can offer: the ability to build financial applications that respect the privacy of the people who use them.
Your users deserve applications that protect their data. Blockchain deserves to grow beyond the limitations of full public transparency. And you, as a developer, have the tools to build that future right now.
Get Started with Fhenix CoFHE
- Documentation: docs.fhenix.io
-
CoFHE SDK on npm:
npm install @cofhe/sdk -
Contracts library:
npm install @fhenixprotocol/cofhe-contracts -
Hardhat Plugin:
npm install @cofhe/hardhat-plugin - GitHub: github.com/FhenixProtocol
- Twitter (X) Community: Follow Fhenix on X (Twitter) to ask questions, share what you build, and connect with other developers working on confidential smart contracts
This tutorial was written based on a working PiggyBank contract deployed on Ethereum Sepolia testnet using CoFHE. Every concept described here reflects real, running code — not theory.
Top comments (0)