docs/solidity-guides/foundry/write_test.md
This page shows how to write FHEVM tests in Foundry using forge-fhevm.
Every FHEVM test contract inherits from FhevmTest. Calling super.setUp() deploys the FHEVM host contracts at their canonical deterministic addresses.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import {FhevmTest} from "forge-fhevm/FhevmTest.sol";
import {FHE} from "@fhevm/solidity/lib/FHE.sol";
import "encrypted-types/EncryptedTypes.sol";
contract MyTest is FhevmTest {
MyContract myContract;
function setUp() public override {
super.setUp(); // deploy FHEVM host contracts
myContract = new MyContract();
}
}
{% hint style="warning" %}
The contract under test must inherit a Zama config (e.g. ZamaEthereumConfig) so FHE.* calls route to the FHEVM host contracts deployed by setUp().
{% endhint %}
Use the encrypt* helpers to build a (handle, proof) pair for any contract that calls FHE.fromExternal.
{% stepper %}
{% step %}
The two-argument overload uses address(this) as the implicit user:
(externalEuint64 amount, bytes memory proof) = encryptUint64(100, address(myContract));
{% endstep %}
{% step %}
The three-argument overload binds the proof to a different user:
address alice = address(0xA11CE);
(externalEuint64 amount, bytes memory proof) = encryptUint64(100, alice, address(myContract));
{% endstep %}
{% step %}
vm.prank(alice);
myContract.deposit(amount, proof);
{% endstep %}
{% endstepper %}
| Function | Value type | Returned handle |
|---|---|---|
encryptBool | bool | externalEbool |
encryptUint8 | uint8 | externalEuint8 |
encryptUint16 | uint16 | externalEuint16 |
encryptUint32 | uint32 | externalEuint32 |
encryptUint64 | uint64 | externalEuint64 |
encryptUint128 | uint128 | externalEuint128 |
encryptUint256 | uint256 | externalEuint256 |
encryptAddress | address | externalEaddress |
{% hint style="info" %}
Each call to encrypt* increments an internal nonce, so encrypting the same value twice produces different handles.
{% endhint %}
forge-fhevm exposes three decryption modes that mirror production decryption flows. Pick the one that matches your contract's pattern.
decrypt(handle) — low-level lookupDirect cleartext for the handle. No ACL or proof checks. Best for unit assertions:
euint64 balance = myContract.balanceHandle(alice);
assertEq(decrypt(balance), 100);
decrypt() has typed overloads for every encrypted type:
bool a = decrypt(myEbool);
uint8 b = decrypt(myEuint8);
uint64 c = decrypt(myEuint64);
address d = decrypt(myEaddress);
publicDecrypt(handles) — KMS-signed public decryptionUse when your contract verifies decryption proofs on-chain via FHE.checkSignatures(). Returns cleartexts and a KMS-signed proof:
bytes32[] memory handles = new bytes32[](1);
handles[0] = euint64.unwrap(balance);
(uint256[] memory cleartexts, bytes memory proof) = publicDecrypt(handles);
FHE.checkSignatures(handles, abi.encode(cleartexts), proof);
assertEq(cleartexts[0], 100);
{% hint style="warning" %}
publicDecrypt() reverts with HandleNotAllowedForPublicDecryption if the contract did not call FHE.makePubliclyDecryptable() on the handle.
{% endhint %}
userDecrypt(handle, user, contract, signature) — user-facing flowThe full user decryption flow with persistent ACL checks and EIP-712 signature verification:
uint256 constant ALICE_PK = 0xA11CE;
address alice = vm.addr(ALICE_PK);
// (mint or transfer that grants ACL to alice through business logic)
bytes memory sig = signUserDecrypt(ALICE_PK, address(myContract));
uint256 cleartext = userDecrypt(
euint64.unwrap(myContract.balanceHandle(alice)),
alice,
address(myContract),
sig
);
assertEq(cleartext, 100);
| Error | Cause |
|---|---|
UserAddressEqualsContractAddress | userAddress == contractAddress |
UserNotAuthorizedForDecrypt | User lacks persistent ACL permission |
ContractNotAuthorizedForDecrypt | Contract lacks persistent ACL permission |
InvalidUserDecryptSignature | Signature does not recover to userAddress |
{% hint style="info" %}
ACL permissions are granted by the contract under test as part of its business logic — for example, when a token's mint calls FHE.allow(balance, owner). You don't need to grant permissions manually in tests.
{% endhint %}
A complete counter test is shipped in fhevm-foundry-template/test/FHECounter.t.sol:
contract FHECounterTest is FhevmTest {
FHECounter counter;
uint256 internal constant ALICE_PK = 0xA11CE;
address alice;
function setUp() public override {
super.setUp();
counter = new FHECounter();
alice = vm.addr(ALICE_PK);
}
function test_incrementTheCounterByOne() public {
(externalEuint32 encOne, bytes memory proof) = encryptUint32(1, alice, address(counter));
vm.prank(alice);
counter.increment(encOne, proof);
bytes memory sig = signUserDecrypt(ALICE_PK, address(counter));
uint256 clear = userDecrypt(euint32.unwrap(counter.getCount()), alice, address(counter), sig);
assertEq(clear, 1);
}
}
forge test -vvv
forge test --match-test test_incrementTheCounterByOne -vvv # single test
🟨 Go to Deploy FHEVM contracts with Foundry to deploy your contract to a local Anvil node or to Sepolia.
🟨 Go to forge-fhevm API reference for the full FhevmTest API.