0.7 release renames permits to ACPs, replaces per-ciphertext input signatures with one signature per batch, and binds every encrypted input to the contract that will consume it. It also moves you to @fhenixprotocol/cofhe-contracts 0.2.0, which deletes the InEuintXX structs. Most projects need Solidity changes, not only TypeScript changes.
There are no deprecation shims. Old names are removed, so the compiler finds most of the work for you.
Three packages move together
This is one migration across three independently versioned packages. Bumping a subset is the most common way to end up with errors that look like SDK bugs.fhenix-confidential-contracts 0.4.0 depends on an exact @fhenixprotocol/cofhe-contracts@0.2.0. Leaving a beta pin in place resolves two copies of FHE.sol, and sharedEuint64 from one is not the same type as sharedEuint64 from the other. The resulting errors read like nonsense, so check the pin first.
If you are coming from cofhejs rather than @cofhe/sdk, follow migrating from cofhejs first. If you are on 0.5.x, the Solidity work below is identical, because the InEuintXX structs did not change between 0.5 and 0.6.
Let the skill do the mechanical work
Most of this migration is mechanical, and the parts that are not deserve a conversation rather than a find and replace. Fhenix publishes an agent skill that drives the whole thing. It detects what your project uses, works in dependency order, shows you a diff before touching anything, and reports what it could not decide for you. The skill uses the open Agent Skill format, which Claude Code, Cursor, GitHub Copilot, VS Code, Codex, Gemini CLI, OpenCode, Roo, Kiro, and Goose all read. Drop the folder wherever your agent looks for skills, commonly.claude/skills/ or .cursor/skills/:
- A function taking two or more encrypted parameters.
- Encrypted inputs produced in one place and consumed in another.
- A contract you do not control on the other side of a handoff.
- Persisted
EncryptedItemInputrecords. They carry per-item signatures, which cannot be rebuilt from a batch signature.
Work contract-first
If you are doing this by hand, the order matters. The ABI you land on decides what every call site has to look like, so going backwards means rewriting the same call sites twice.1
Bump the three packages
Resolve versions from the registry rather than copying them from a guide. Update the contract dependencies at the same time as the JavaScript ones.
2
Change your contracts
Move off the deleted
InEuintXX structs, then decide which values cross a contract boundary.3
Update config keys
These throw at client construction, so they surface the moment you boot.
4
Rename permits to ACPs
Sweeping the renames first makes every remaining TypeScript error a genuine shape problem rather than a missing identifier.
5
Update encrypt call sites
The batch result and the consuming contract land on the same lines, so do them together.
Contracts: inputs from a user
0.2.0 deletes the InEuintXX structs, the FHE.asEuint32(InEuint32) overloads, the Utils.inputFromEuint32 helpers, and ITaskManager.verifyInput. An encrypted value arriving from a user is now an externalEuintXX handle plus a bytes proof.
bytes parameter that immediately follows the handle it authenticates. This changes the ABI, so a contract coming from InEuintXX needs a redeploy.
A contract that already takes (externalEuint32, bytes) for a single encrypted value needs no change and no redeploy. FHE.asEuint32(handle, proof) verifies that input as a batch of one, so a 0.7 signature works against the ABI you already deployed.
The proof does not have to be the last parameter. It has to follow the
external* handle as a pair. Extra plain arguments can come after it, which is how ERC-7984’s confidentialTransferAndCall is shaped.More than one encrypted value
One signature now covers all the handles in the batch, computed over them together. You cannot keep two separate(handle, proof) pairs, because verifying one handle against a signature covering two reverts.
You can still keep the parameter names, which most projects prefer to an array:
function transfer(address to, externalEuint32[] calldata values, bytes calldata signature), is shorter but collapses named parameters into indices. Either way the encrypted parameters must be adjacent, because they share one signature and there is no other way to tell which bytes belongs to them.
For a batch mixing types, such as a euint32 with an ebool, call ITaskManager.batchVerifyInputs directly.
Values that cross a contract boundary
0.2.0 adds a sharedEuintXX type for encrypted values passed between contracts. Both directions count: a value handed over as an argument, and a value returned by a function that is not view. A view function returning an encrypted value is unaffected, because it never granted anything.
This is the part of the migration the compiler cannot help with. The 0.6 spelling, an FHE.allowTransient grant plus a bare euintXX parameter, still compiles and still runs while both sides stay on it.
It is also the part with a security consequence. A function taking a bare euintXX from outside can be turned into an oracle over every ciphertext the contract holds. FHE operations check the permission of the contract performing them, not of whoever called it. An attacker passes a handle the contract is allowed on, such as one read from its own storage, and gets back a value derived from it.
pull(euint64) and pull(sharedEuint64) are both bytes32 on the wire, so an unmigrated caller compiles against a migrated callee and then reverts at runtime with NotShared.
Pick the receive form by how the value reached you. receiveEuint64Param checks the sharer against msg.sender and suits a value that arrived as an argument. receiveEuint64FromCall(shared, callee) checks it against the contract you called, and callee must be the address called in that same expression.
Returning an encrypted value works the same way in reverse. Share the result with msg.sender, and unwrap it with the FromCall form:
FHE.shareEuint64 reverts with SenderNotAllowed unless your contract is itself allowed on the handle. You cannot share what you cannot use.
Sharing is single-use and transaction-scoped, so a share cannot be stored, replayed, or reconstructed from an event. To keep a received value past the transaction, call FHE.allowThis on the unwrapped euintXX. Anything you derive from it produces a new handle that needs its own FHE.allowThis before you store it.
Config keys
Five keys were renamed. In0.6 an unknown key was silently dropped and the setting fell back to its default. Both schemas now reject unknown keys and name the replacement, so a stale key throws when you construct the client.
If your
0.6 app set any of these, the setting was already being ignored and the default was in force. Behavior can change once it starts applying again. An expiration you believed was one day may have been running at the 30 day default.Permits are now ACPs
“Permit” is now “ACP”, short for Access Control Permission, so that it is not confused with an ERC-2612 permit. The entry point moves and the client namespace is singular:.withPermit() becomes .withACP() and .withoutPermit() becomes .withoutACP().
React hooks rename the same way. useCofhePermits becomes useCofheACPs, useCofheActivePermit becomes useCofheActiveACP, and the rest of the family follows the pattern.
Rename only identifiers that resolve to a @cofhe/* import. A blind replace of Permit corrupts unrelated code, and the English words permitted and permitting are not renames. isPermittedCofheEnvironment and isAllowedWithPermission keep their names.
For the full type table, the new scope model, and what the client gained, see Access Control Permissions.
Encrypt call sites
Two changes land on the same lines.execute() returns a different shape, and you must now declare the consuming contract.
inputs.length + 1 elements. Code that assumed the result matched the input count is off by one.
setConsumingContract is required because the verifier binds the target contract into the signed digest, which stops a batch signed for one contract being replayed into another. Omitting it is a compile error in TypeScript, since encryptInputs() returns a builder without an execute() method.
The per-item input types are gone: EncryptedItemInput, EncryptedUint64Input, and the rest of that family. A value that used to be one of them is now a handle. These also break on your own helpers, where a fixture typed (encAmount: EncryptedUint64Input) fails at its definition rather than at the call site. asHashPlusProof() is removed, because its output is what execute() always returns now.
Silent changes
A clean build proves very little in this migration. Each of these compiles and then behaves differently:
Verify by exercising a round trip, not by compiling. For every bare-handle function you kept, ask whether an arbitrary caller can reach it with a handle the contract is allowed on. If they can, guard it with
FHE.isAllowed(value, msg.sender).
Next steps
- Check every version against the compatibility page.
- Read the Access Control Permissions guide for the permission model in full.
- Review encrypting inputs for the current builder API.