Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Sharing Encrypted Values Between Contracts

Passing an encrypted handle to another contract is easy — it is a bytes32. Letting that contract compute on it is the part that needs care, because the ACL decides who may use a ciphertext and by default only your contract can.

sharedEuintXX carries a handle and permission to use it, for one transaction. Nothing is decrypted or re-encrypted; the receiver gets access to the same ciphertext.

Which type do I need?

TypeComes fromUsed for
externalEuint64a user, offchainencrypted input submitted with a proof
sharedEuint64another contracthanding a value to a contract in this transaction
euint64your own storage or computationeverything you do internally

One shared* type exists per encrypted type: sharedEbool, sharedEuint8, sharedEuint16, sharedEuint32, sharedEuint64, sharedEuint128, sharedEaddress.

When to use it

You want to…Use
Accept an encrypted value from a userexternalEuint64 + FHE.asEuint64(value, proof)
Hand a value to another contract in this transactionsharedEuint64 + share / receive
Keep using a value in a later transactionFHE.allowThis(value)
Let another address use a value in a later transactionFHE.allow(value, account)
Make a value readable by anyoneFHE.allowGlobal(value)

The deciding factor is lifetime: sharing lasts one transaction, the allow* family is persistent.

Usage

Vault.sol
contract Vault {
    function deposit(Token token, externalEuint64 inAmount, bytes memory proof) external {
        euint64 amount = FHE.asEuint64(inAmount, proof);
        token.accept(FHE.shareEuint64(amount, address(token)));
    }
}
Token.sol
contract Token {
    function accept(sharedEuint64 shared) external {
        euint64 amount = FHE.receiveEuint64Param(shared);
        // ... compute with `amount`
    }
}

FHE.shareEuint64 reverts unless your contract is allowed on the handle — you cannot share what you cannot use.

Keeping a received value

A received value is transient: usable for the rest of this transaction, gone afterwards. To keep it, call FHE.allowThis on the unwrapped euint64 — not on the sharedEuint64:

Token.sol
mapping(address => euint64) public deposits;
 
function accept(sharedEuint64 shared) external {
    euint64 amount = FHE.receiveEuint64Param(shared);
 
    FHE.allowThis(amount);
    deposits[msg.sender] = amount;
}

The same applies to anything derived from a received value: FHE.add(balance, amount) produces a new handle that needs its own FHE.allowThis before being stored.

Choosing the receive function

Which one you need depends only on how the value reached you:

The value arrived as…Use
a parameter of the function you are inreceiveEuint64Param(shared)
the return value of a call you madereceiveEuint64FromCall(shared, callee)

receiveEuint64Param needs no address — use it whenever the value came in through your function signature.

Returning a value

Share the result back with msg.sender, and unwrap it with FromCall:

Token.sol
function swap(sharedEuint64 shared) external returns (sharedEuint64) {
    euint64 amountIn = FHE.receiveEuint64Param(shared);
    return FHE.shareEuint64(FHE.div(amountIn, FHE.asEuint64(2)), msg.sender);
}
Vault.sol
euint64 out = FHE.receiveEuint64FromCall(token.swap(shared), address(token));
FHE.allowThis(out);

Limits

  • One transaction. A sharedEuintXX cannot be stored and received later.
  • One use. The first successful receive consumes it.
  • One receiver. Only the address named in shareEuint64 can receive it.
  • No batching. Each share and each receive is one TaskManager call.

Errors

RevertMeaning
SenderNotAllowed(sharer)You shared a handle your contract is not allowed to use.
NotShared(handle, receiver)Nothing shared with you for this handle — never shared, already consumed, shared with a different address, or an earlier transaction.
UnexpectedSharer(expected, actual)A share exists but came from a different contract than the one named. Usually the wrong receive function, or the wrong address passed to FromCall.

Access control is still yours

Receiving proves the caller deliberately handed you the value. It says nothing about whether that caller should be talking to you:

require(msg.sender == address(officialVault), "token: unknown vault"); // who may call
euint64 amount = FHE.receiveEuint64Param(shared);                      // did they really share it

Relaying

A share names one receiver and must be handed over by the contract that created it — "A shares with C, B passes it along" does not work. Chain the hops instead: each contract receives, then shares onward.

euint64 value = FHE.receiveEuint64Param(fromA);
c.consume(FHE.shareEuint64(value, address(c)));