|
| 1 | +This example demonstrate how to build an confidential counter using FHEVM, in comparison to a simple counter. |
| 2 | + |
| 3 | +{% hint style="info" %} |
| 4 | +To run this example correctly, make sure the files are placed in the following directories: |
| 5 | +- `.sol` file → `<your-project-root-dir>/contracts/` |
| 6 | +- `.ts` file → `<your-project-root-dir>/test/` |
| 7 | + |
| 8 | +This ensures Hardhat can compile and test your contracts as expected. |
| 9 | +{% endhint %} |
| 10 | + |
| 11 | +## A simple counter |
| 12 | +{% tabs %} |
| 13 | + |
| 14 | +{% tab title="counter.sol" %} |
| 15 | +```solidity |
| 16 | +// SPDX-License-Identifier: MIT |
| 17 | +pragma solidity ^0.8.24; |
| 18 | +
|
| 19 | +/// @title A simple counter contract |
| 20 | +contract Counter { |
| 21 | + uint32 private _count; |
| 22 | +
|
| 23 | + /// @notice Returns the current count |
| 24 | + function getCount() external view returns (uint32) { |
| 25 | + return _count; |
| 26 | + } |
| 27 | +
|
| 28 | + /// @notice Increments the counter by 1 |
| 29 | + function increment(uint32 value) external { |
| 30 | + _count += value; |
| 31 | + } |
| 32 | +
|
| 33 | + /// @notice Decrements the counter by 1 |
| 34 | + function decrement(uint32 value) external { |
| 35 | + require(_count > value, "Counter: cannot decrement below zero"); |
| 36 | + _count -= value; |
| 37 | + } |
| 38 | +} |
| 39 | +``` |
| 40 | +{% endtab %} |
| 41 | + |
| 42 | +{% tab title="counter.ts" %} |
| 43 | +```ts |
| 44 | +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; |
| 45 | +import { ethers } from "hardhat"; |
| 46 | +import { Counter, Counter__factory } from "../types"; |
| 47 | +import { expect } from "chai"; |
| 48 | + |
| 49 | +type Signers = { |
| 50 | + deployer: HardhatEthersSigner; |
| 51 | + alice: HardhatEthersSigner; |
| 52 | + bob: HardhatEthersSigner; |
| 53 | +}; |
| 54 | + |
| 55 | +async function deployFixture() { |
| 56 | + const factory = (await ethers.getContractFactory("Counter")) as Counter__factory; |
| 57 | + const counterContract = (await factory.deploy()) as Counter; |
| 58 | + const counterContractAddress = await counterContract.getAddress(); |
| 59 | + |
| 60 | + return { counterContract, counterContractAddress }; |
| 61 | +} |
| 62 | + |
| 63 | +describe("Counter", function () { |
| 64 | + let signers: Signers; |
| 65 | + let counterContract: Counter; |
| 66 | + |
| 67 | + before(async function () { |
| 68 | + const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); |
| 69 | + signers = { deployer: ethSigners[0], alice: ethSigners[1], bob: ethSigners[2] }; |
| 70 | + }); |
| 71 | + |
| 72 | + beforeEach(async () => { |
| 73 | + ({ counterContract } = await deployFixture()); |
| 74 | + }); |
| 75 | + |
| 76 | + it("count should be zero after deployment", async function () { |
| 77 | + const count = await counterContract.getCount(); |
| 78 | + console.log(`Counter.getCount() === ${count}`); |
| 79 | + // Expect initial count to be 0 after deployment |
| 80 | + expect(count).to.eq(0); |
| 81 | + }); |
| 82 | + |
| 83 | + it("increment the counter by 1", async function () { |
| 84 | + const countBeforeInc = await counterContract.getCount(); |
| 85 | + const tx = await counterContract.connect(signers.alice).increment(1); |
| 86 | + await tx.wait(); |
| 87 | + const countAfterInc = await counterContract.getCount(); |
| 88 | + expect(countAfterInc).to.eq(countBeforeInc + 1n); |
| 89 | + }); |
| 90 | + |
| 91 | + it("decrement the counter by 1", async function () { |
| 92 | + // First increment, count becomes 1 |
| 93 | + let tx = await counterContract.connect(signers.alice).increment(); |
| 94 | + await tx.wait(); |
| 95 | + // Then decrement, count goes back to 0 |
| 96 | + tx = await counterContract.connect(signers.alice).decrement(1); |
| 97 | + await tx.wait(); |
| 98 | + const count = await counterContract.getCount(); |
| 99 | + expect(count).to.eq(0); |
| 100 | + }); |
| 101 | +}); |
| 102 | +``` |
| 103 | +{% endtab %} |
| 104 | + |
| 105 | +{% endtabs %} |
| 106 | + |
| 107 | +## An FHE counter |
| 108 | + |
| 109 | +{% tabs %} |
| 110 | + |
| 111 | +{% tab title="FHECounter.sol" %} |
| 112 | +```solidity |
| 113 | +// SPDX-License-Identifier: MIT |
| 114 | +pragma solidity ^0.8.24; |
| 115 | +
|
| 116 | +import {FHE, euint32, externalEuint32} from "@fhevm/solidity/lib/FHE.sol"; |
| 117 | +import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; |
| 118 | +
|
| 119 | +/// @title A simple FHE counter contract |
| 120 | +contract FHECounter is SepoliaConfig { |
| 121 | + euint32 private _count; |
| 122 | +
|
| 123 | + /// @notice Returns the current count |
| 124 | + function getCount() external view returns (euint32) { |
| 125 | + return _count; |
| 126 | + } |
| 127 | +
|
| 128 | + /// @notice Increments the counter by a specified encrypted value. |
| 129 | + /// @dev This example omits overflow/underflow checks for simplicity and readability. |
| 130 | + /// In a production contract, proper range checks should be implemented. |
| 131 | + function increment(externalEuint32 inputEuint32, bytes calldata inputProof) external { |
| 132 | + euint32 encryptedEuint32 = FHE.fromExternal(inputEuint32, inputProof); |
| 133 | +
|
| 134 | + _count = FHE.add(_count, encryptedEuint32); |
| 135 | +
|
| 136 | + FHE.allowThis(_count); |
| 137 | + FHE.allow(_count, msg.sender); |
| 138 | + } |
| 139 | +
|
| 140 | + /// @notice Decrements the counter by a specified encrypted value. |
| 141 | + /// @dev This example omits overflow/underflow checks for simplicity and readability. |
| 142 | + /// In a production contract, proper range checks should be implemented. |
| 143 | + function decrement(externalEuint32 inputEuint32, bytes calldata inputProof) external { |
| 144 | + euint32 encryptedEuint32 = FHE.fromExternal(inputEuint32, inputProof); |
| 145 | +
|
| 146 | + _count = FHE.sub(_count, encryptedEuint32); |
| 147 | +
|
| 148 | + FHE.allowThis(_count); |
| 149 | + FHE.allow(_count, msg.sender); |
| 150 | + } |
| 151 | +} |
| 152 | +``` |
| 153 | +{% endtab %} |
| 154 | + |
| 155 | +{% tab title="FHECounter.ts" %} |
| 156 | +```ts |
| 157 | +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; |
| 158 | +import { ethers, fhevm } from "hardhat"; |
| 159 | +import { FHECounter, FHECounter__factory } from "../types"; |
| 160 | +import { expect } from "chai"; |
| 161 | +import { FhevmType } from "@fhevm/hardhat-plugin"; |
| 162 | + |
| 163 | +type Signers = { |
| 164 | + deployer: HardhatEthersSigner; |
| 165 | + alice: HardhatEthersSigner; |
| 166 | + bob: HardhatEthersSigner; |
| 167 | +}; |
| 168 | + |
| 169 | +async function deployFixture() { |
| 170 | + const factory = (await ethers.getContractFactory("FHECounter")) as FHECounter__factory; |
| 171 | + const fheCounterContract = (await factory.deploy()) as FHECounter; |
| 172 | + const fheCounterContractAddress = await fheCounterContract.getAddress(); |
| 173 | + |
| 174 | + return { fheCounterContract, fheCounterContractAddress }; |
| 175 | +} |
| 176 | + |
| 177 | +describe("FHECounter", function () { |
| 178 | + let signers: Signers; |
| 179 | + let fheCounterContract: FHECounter; |
| 180 | + let fheCounterContractAddress: string; |
| 181 | + |
| 182 | + before(async function () { |
| 183 | + const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); |
| 184 | + signers = { deployer: ethSigners[0], alice: ethSigners[1], bob: ethSigners[2] }; |
| 185 | + }); |
| 186 | + |
| 187 | + beforeEach(async () => { |
| 188 | + ({ fheCounterContract, fheCounterContractAddress } = await deployFixture()); |
| 189 | + }); |
| 190 | + |
| 191 | + it("encrypted count should be uninitialized after deployment", async function () { |
| 192 | + const encryptedCount = await fheCounterContract.getCount(); |
| 193 | + // Expect initial count to be bytes32(0) after deployment, |
| 194 | + // (meaning the encrypted count value is uninitialized) |
| 195 | + expect(encryptedCount).to.eq(ethers.ZeroHash); |
| 196 | + }); |
| 197 | + |
| 198 | + it("increment the counter by 1", async function () { |
| 199 | + const encryptedCountBeforeInc = await fheCounterContract.getCount(); |
| 200 | + expect(encryptedCountBeforeInc).to.eq(ethers.ZeroHash); |
| 201 | + const clearCountBeforeInc = 0; |
| 202 | + |
| 203 | + // Encrypt constant 1 as a euint32 |
| 204 | + const clearOne = 1; |
| 205 | + const encryptedOne = await fhevm |
| 206 | + .createEncryptedInput(fheCounterContractAddress, signers.alice.address) |
| 207 | + .add32(clearOne) |
| 208 | + .encrypt(); |
| 209 | + |
| 210 | + const tx = await fheCounterContract |
| 211 | + .connect(signers.alice) |
| 212 | + .increment(encryptedOne.handles[0], encryptedOne.inputProof); |
| 213 | + await tx.wait(); |
| 214 | + |
| 215 | + const encryptedCountAfterInc = await fheCounterContract.getCount(); |
| 216 | + const clearCountAfterInc = await fhevm.userDecryptEuint( |
| 217 | + FhevmType.euint32, |
| 218 | + encryptedCountAfterInc, |
| 219 | + fheCounterContractAddress, |
| 220 | + signers.alice, |
| 221 | + ); |
| 222 | + |
| 223 | + expect(clearCountAfterInc).to.eq(clearCountBeforeInc + clearOne); |
| 224 | + }); |
| 225 | + |
| 226 | + it("decrement the counter by 1", async function () { |
| 227 | + // Encrypt constant 1 as a euint32 |
| 228 | + const clearOne = 1; |
| 229 | + const encryptedOne = await fhevm |
| 230 | + .createEncryptedInput(fheCounterContractAddress, signers.alice.address) |
| 231 | + .add32(clearOne) |
| 232 | + .encrypt(); |
| 233 | + |
| 234 | + // First increment by 1, count becomes 1 |
| 235 | + let tx = await fheCounterContract |
| 236 | + .connect(signers.alice) |
| 237 | + .increment(encryptedOne.handles[0], encryptedOne.inputProof); |
| 238 | + await tx.wait(); |
| 239 | + |
| 240 | + // Then decrement by 1, count goes back to 0 |
| 241 | + tx = await fheCounterContract.connect(signers.alice).decrement(encryptedOne.handles[0], encryptedOne.inputProof); |
| 242 | + await tx.wait(); |
| 243 | + |
| 244 | + const encryptedCountAfterDec = await fheCounterContract.getCount(); |
| 245 | + const clearCountAfterInc = await fhevm.userDecryptEuint( |
| 246 | + FhevmType.euint32, |
| 247 | + encryptedCountAfterDec, |
| 248 | + fheCounterContractAddress, |
| 249 | + signers.alice, |
| 250 | + ); |
| 251 | + |
| 252 | + expect(clearCountAfterInc).to.eq(0); |
| 253 | + }); |
| 254 | +}); |
| 255 | +``` |
| 256 | +{% endtab %} |
| 257 | + |
| 258 | +{% endtabs %} |
0 commit comments