fhevm-cross-contract
Cross-Contract Encrypted Handle Passing
Use this skill when your architecture involves more than one contract operating on encrypted values. Every time an encrypted handle crosses a contract boundary, it needs an explicit ACL grant. Forgetting a single grant in a multi-hop flow breaks the chain at the first real ACL boundary.
When To Use
- Contract A computes an encrypted result and Contract B uses it
- Factory contracts deploy new contracts with encrypted initial state
- DeFi composability where encrypted amounts flow through routers, pools, or vaults
- Debugging "handle not accessible" or ACL failures in multi-contract flows
Core Mental Model
An encrypted handle is a capability token with an explicit access list. Passing it to another contract hands over the token but NOT the access. Every hop needs its own ACL grant. Think of it as a permission chain: A grants to B, B computes a new result and grants to C. Each new computation produces a fresh handle that does not inherit prior persistent grants.
Hard Constraints
- Passing a handle as a function parameter does NOT grant the callee access to use it.
FHE.allowTransient(handle, target)grants access for the current transaction only.FHE.allow(handle, target)grants persistent access across transactions.- A new handle from any FHE operation does not inherit prior persistent grants.
- ACL failures in cross-contract calls fail at the contract boundary, typically through an ACL error or an inaccessible downstream handle.
The Two Grant Types
Transient: Immediate Downstream Call
contract Router {
function swap(euint64 encryptedAmount, address pool) external {
euint64 netAmount = FHE.sub(encryptedAmount, fee);
FHE.allowTransient(netAmount, pool); // pool needs it NOW
IPool(pool).executeSwap(msg.sender, netAmount);
}
}
Persistent: Stored for Later Use
contract Vault {
function deposit(euint64 encryptedAmount) external {
euint64 shares = computeShares(encryptedAmount);
FHE.allowThis(shares);
_shares[msg.sender] = shares;
FHE.allow(shares, address(rewardContract)); // reward contract uses it later
}
}
Permission Chain Pattern
For multi-hop flows, document and implement the full chain. Each contract grants access to the next hop; each new FHE operation produces a new handle that needs its own downstream grants.
Thread the originating user's address explicitly through the chain. Never rely on
tx.origin for ACL grants: it is a known phishing footgun, and an intermediate
contract may be called from another contract, so tx.origin is not guaranteed to
be the user you think it is.
contract ContractA {
function process(euint64 input) external {
euint64 result = FHE.mul(input, rate);
FHE.allowTransient(result, address(contractB));
contractB.process(result, msg.sender);
}
}
contract ContractB {
function process(euint64 input, address user) external {
euint64 result = FHE.add(input, bonus);
FHE.allowTransient(result, address(contractC));
contractC.finalize(result, user);
}
}
contract ContractC {
function finalize(euint64 input, address user) external {
euint64 finalResult = FHE.sub(input, fee);
FHE.allowThis(finalResult);
FHE.allow(finalResult, user);
_results[user] = finalResult;
}
}
Permission Chain Documentation Template
For any multi-contract flow, document every hop as a table:
| Step | Contract | Operation | Input Handle ACL | Output Handle | Output ACL | Grant |
|---|---|---|---|---|---|---|
| 1 | A | fromExternal |
n/a | h1 | [A] | -- |
| 2 | A | mul(h1, rate) |
[A] | h2 | [A] | -- |
| 3 | A | allowTransient(h2, B) |
-- | h2 | [A, B*] | transient to B |
| 4 | B | sub(h2, fee) |
[A, B*] | h3 | [B] | -- |
| 5 | B | allowTransient(h3, C) |
-- | h3 | [B, C*] | transient to C |
| 6 | C | computation on h3 | [B, C*] | h4 | [C] | -- |
| 7 | C | allowThis(h4) |
-- | h4 | [C] | persistent to C |
| 8 | C | allow(h4, user) |
-- | h4 | [C, user] | persistent to user |
If any row is missing a required grant, the chain breaks at that boundary.
allowTransient vs allow Scenario Matrix
| Scenario | Grant Type | Reason |
|---|---|---|
| Immediate call to another contract in same tx | allowTransient |
Access needed only now |
| Handle stored in receiving contract for later use | allow (persistent) |
Must survive end-of-tx |
| User needs to decrypt a result via SDK | allow(handle, user) |
Persistent read access |
| Another contract reads stored handle in a future tx | allow(handle, contract) |
Cross-tx access |
| Factory initializing a new contract | allow(handle, newContract) |
New contract stores it |
| Contract stores its own computed result | allowThis(handle) |
Self-access across txs |
Factory Pattern
Factories must grant ACL to the newly created contract before it can use encrypted state:
contract VaultFactory {
function createVault(euint64 initialDeposit) external returns (address) {
Vault vault = new Vault();
FHE.allow(initialDeposit, address(vault)); // grant BEFORE initialize
vault.initialize(initialDeposit, msg.sender);
return address(vault);
}
}
contract Vault {
function initialize(euint64 initialDeposit, address owner) external {
FHE.allowThis(initialDeposit); // vault stores it for future txs
FHE.allow(initialDeposit, owner);
_balances[owner] = initialDeposit;
}
}
DeFi Composability: DEX Router Example
contract DEXRouter {
function swap(externalEuint64 encAmount, bytes calldata proof, address pool) external {
euint64 amount = FHE.fromExternal(encAmount, proof);
FHE.allowTransient(amount, pool);
IPool(pool).swap(msg.sender, amount, msg.sender);
}
}
contract Pool {
function swap(address sender, euint64 amountIn, address recipient) external {
euint64 amountOut = computeOutput(amountIn);
FHE.allowThis(amountOut);
FHE.allow(amountOut, recipient); // recipient can decrypt
}
}
Anti-Patterns
Anti-Pattern 1: Pass Handle Without Granting Access
Calling target.doSomething(handle) without allowTransient or allow. Handle arrives, every operation fails.
Anti-Pattern 2: allowTransient for Stored Handles
Granting transient access for a handle the target stores. Access expires end-of-transaction.
Anti-Pattern 3: Assume New Handles Inherit Permissions
FHE.add(handle, x) produces a fresh result handle. Prior persistent grants on the input do not carry over.
Anti-Pattern 4: Factory Forgets Created Contract
Factory deploys, passes encrypted state, but never calls FHE.allow(handle, newContract).
Anti-Pattern 5: Missing Intermediate Grants
In A -> B -> C, A grants to B but B forgets to grant to C. The flow breaks at B-to-C.
Review Checklist
- At every contract boundary, is there an explicit
allowTransientorallow? - Is the grant type correct? Transient for same-tx, persistent for stored handles.
- After every FHE operation in the receiver, does the result get
allowThis? - In factory patterns, does the factory grant ACL before initialization completes?
- In multi-hop flows, does every intermediate contract grant to the next hop?
- Is the full permission chain documented for reviewers?
Output Expectations
When applying this skill, structure analysis around:
- the full contract call graph involving encrypted handles
- which handles cross which boundaries
- which grant type is appropriate at each boundary
- where grants are missing or wrong
Related Skills
skills/fhevm-acl-lifecycle/SKILL.md— the grant primitives this skill composesskills/fhevm-encrypted-inputs/SKILL.md—externalEuint64vs onchaineuint64at the boundary
More from z-korp/fhevm-cookbook
fhevm-router
Routes Zama FHEVM tasks to the right official docs path and next step
10fhevm-testing
Use when writing, structuring, or debugging tests for FHEVM contracts. Covers mocked mode vs real protocol, Hardhat decrypt helpers, input encryption in tests, and the false-confidence gap between local and testnet behavior.
10fhevm-acl-lifecycle
Use when granting, auditing, or debugging ACL permissions on encrypted handles in FHEVM. Covers FHE.allow, FHE.allowThis, FHE.allowTransient, and the critical rule that new handles do not inherit prior persistent ACL grants.
10fhevm-control-flow
Use when replacing if/else, require, or any conditional logic that depends on encrypted values in FHEVM. Covers FHE.select as the inline branching primitive, fallback semantics on encrypted conditions, and async public decryption when logic must branch back to plaintext state.
10oz-utils-safemath
Use when you need overflow-safe encrypted arithmetic on euint64 values. Covers the OpenZeppelin FHESafeMath library (tryIncrease, tryDecrease, tryAdd, trySub), uninitialized-handle semantics, and when to prefer it over raw FHE.add / FHE.sub.
10fhevm-public-decryption
Use when implementing two-step public decryption for state-changing operations in FHEVM. Covers makePubliclyDecryptable, off-chain proof retrieval, onchain verification with checkSignatures, and the critical single-step unwrap bug.
10