Permissions

Permission management anti-patterns in FHE development. Covers mistakes with allowThis, allow, and permission propagation across transfers and cross-contract calls.

circle-info

To run this example correctly, make sure the files are placed in the following directories:

  • .sol file → <your-project-root-dir>/contracts/

  • .ts file → <your-project-root-dir>/test/

This ensures Hardhat can compile and test your contracts as expected.

chevron-right🔐 FHE API Reference (10 items)hashtag

Types: euint32 · externalEuint32

Functions:

  • FHE.add() - Homomorphic addition: result = a + b (overflow wraps)

  • FHE.allow() - Grants PERMANENT permission for address to decrypt/use value

  • FHE.allowThis() - Grants contract permission to operate on ciphertext

  • FHE.allowTransient() - Grants TEMPORARY permission (expires at tx end)

  • FHE.asEuint32() - Encrypts a plaintext uint32 value into euint32

  • FHE.fromExternal() - Validates and converts external encrypted input using inputProof

  • FHE.mul() - Homomorphic multiplication: result = a * b

  • FHE.sub() - Homomorphic subtraction: result = a - b (underflow wraps)

// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.24;

import {FHE, euint32, externalEuint32} from "@fhevm/solidity/lib/FHE.sol";
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";

/**
 * @notice Permission management anti-patterns in FHE development.
 *         Covers mistakes with allowThis, allow, and permission propagation
 *         across transfers and cross-contract calls.
 *
 * @dev Explores missing permissions, view function failures, and delegation issues.
 */
contract FHEPermissionsAntiPatterns is ZamaEthereumConfig {
    euint32 private _secretValue;
    mapping(address => euint32) private _balances;

    // ═══════════════════════════════════════════════════════════════════════
    // ANTI-PATTERN 1: Missing allowThis After Computation
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * ❌ WRONG: Compute but forget allowThis
     * @dev Result exists but contract can't use it in future operations
     */
    function wrongMissingAllowThis(
        externalEuint32 input,
        bytes calldata inputProof
    ) external {
        _secretValue = FHE.fromExternal(input, inputProof);
        euint32 doubled = FHE.mul(_secretValue, FHE.asEuint32(2));
        _secretValue = doubled;

        // ❌ Missing FHE.allowThis! Contract can't use this value later
        FHE.allow(_secretValue, msg.sender);
    }

    /**
     * ✅ CORRECT: Always grant allowThis after computation
     * @dev Contract needs permission to use encrypted values
     */
    function correctWithAllowThis(
        externalEuint32 input,
        bytes calldata inputProof
    ) external {
        _secretValue = FHE.fromExternal(input, inputProof);
        euint32 doubled = FHE.mul(_secretValue, FHE.asEuint32(2));
        _secretValue = doubled;

        FHE.allowThis(_secretValue);
        FHE.allow(_secretValue, msg.sender);
    }

    // ═══════════════════════════════════════════════════════════════════════
    // ANTI-PATTERN 2: Missing allow(user)
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * ❌ WRONG: Only allowThis without user permission
     * @dev No one can decrypt the value
     */
    function wrongMissingUserAllow(
        externalEuint32 input,
        bytes calldata inputProof
    ) external {
        _secretValue = FHE.fromExternal(input, inputProof);

        // ❌ Contract can compute but no one can decrypt!
        FHE.allowThis(_secretValue);
    }

    /**
     * ✅ CORRECT: Grant both allowThis and allow(user)
     * @dev User can decrypt after contract operations
     */
    function correctWithUserAllow(
        externalEuint32 input,
        bytes calldata inputProof
    ) external {
        _secretValue = FHE.fromExternal(input, inputProof);

        FHE.allowThis(_secretValue);
        FHE.allow(_secretValue, msg.sender);
    }

    // ═══════════════════════════════════════════════════════════════════════
    // ANTI-PATTERN 3: View Function Without Permissions
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * ❌ WRONG: Store value without granting permission to caller
     * @dev When caller tries to get value via view, they can't decrypt it
     */
    function wrongStoreWithoutPermission(
        externalEuint32 input,
        bytes calldata inputProof
    ) external {
        _secretValue = FHE.fromExternal(input, inputProof);

        // ❌ Only allowThis, caller has no permission!
        FHE.allowThis(_secretValue);
    }

    /**
     * ✅ CORRECT: Grant permission to caller when storing
     * @dev Caller can now decrypt value returned from view function
     */
    function correctStoreWithPermission(
        externalEuint32 input,
        bytes calldata inputProof
    ) external {
        _secretValue = FHE.fromExternal(input, inputProof);

        FHE.allowThis(_secretValue);
        FHE.allow(_secretValue, msg.sender); // ✅ Grant permission!
    }

    /// @notice View function to get stored value
    function getValue() external view returns (euint32) {
        return _secretValue;
    }

    // ═══════════════════════════════════════════════════════════════════════
    // ANTI-PATTERN 4: Unauthenticated Re-encryption
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * ❌ WRONG: Re-encrypt without verifying public key ownership
     * @dev Anyone can provide any public key and steal encrypted data
     */
    function wrongReencryptWithoutAuth(
        bytes32 publicKey
    ) external view returns (bytes memory) {
        // ❌ SECURITY RISK: No verification that caller owns this public key!
        // Attacker can provide victim's public key and get their data

        // This would allow impersonation attacks:
        // return Gateway.reencrypt(_secretValue, publicKey);

        return ""; // Placeholder
    }

    /**
     * ✅ CORRECT: Use FHEVM's built-in authentication
     * @dev fhevm.js SDK verifies EIP-712 signature automatically
     *      Only the owner of the public key can decrypt
     */
    function correctReencryptWithAuth() external view returns (euint32) {
        // ✅ Return handle directly
        // Client uses fhevm.instance.reencrypt() which:
        // 1. Signs request with their private key (EIP-712)
        // 2. Gateway verifies signature matches public key
        // 3. Only then re-encrypts for that public key

        return _secretValue;
    }

    // ═══════════════════════════════════════════════════════════════════════
    // ANTI-PATTERN 5: Transfer Without Permission Propagation
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * @notice Initialize balance for msg.sender
     * @dev Required before using transfer functions
     */
    function initializeBalance(
        externalEuint32 initialBalance,
        bytes calldata inputProof
    ) external {
        _balances[msg.sender] = FHE.fromExternal(initialBalance, inputProof);
        FHE.allowThis(_balances[msg.sender]);
        FHE.allow(_balances[msg.sender], msg.sender);
    }

    /**
     * ❌ WRONG: Transfer without granting permissions
     * @dev Recipient gets balance but can't use or decrypt it
     */
    function wrongTransferWithoutPermission(
        address recipient,
        externalEuint32 amount,
        bytes calldata inputProof
    ) external {
        euint32 transferAmount = FHE.fromExternal(amount, inputProof);

        _balances[msg.sender] = FHE.sub(_balances[msg.sender], transferAmount);
        _balances[recipient] = FHE.add(_balances[recipient], transferAmount);

        // ❌ Recipient has no permission to use their new balance!
    }

    /**
     * ✅ CORRECT: Grant permissions after transfer
     * @dev Both parties can use and decrypt their updated balances
     */
    function correctTransferWithPermission(
        address recipient,
        externalEuint32 amount,
        bytes calldata inputProof
    ) external {
        euint32 transferAmount = FHE.fromExternal(amount, inputProof);

        _balances[msg.sender] = FHE.sub(_balances[msg.sender], transferAmount);
        _balances[recipient] = FHE.add(_balances[recipient], transferAmount);

        // ✅ Grant permissions to both parties
        FHE.allowThis(_balances[msg.sender]);
        FHE.allow(_balances[msg.sender], msg.sender);
        FHE.allowThis(_balances[recipient]);
        FHE.allow(_balances[recipient], recipient);
    }

    // ═══════════════════════════════════════════════════════════════════════
    // ANTI-PATTERN 6: Cross-Contract Permission Delegation
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * ❌ WRONG: Call another contract without granting permission
     * @dev Other contract can't use the encrypted value
     */
    function wrongCrossContractCall(address processor) external returns (bool) {
        // ❌ processor contract has no permission to use _secretValue!
        // This call will fail or return garbage
        (bool success, ) = processor.call(
            abi.encodeWithSignature("process(uint256)", _secretValue)
        );
        return success;
    }

    /**
     * ✅ CORRECT: Grant temporary permission before cross-contract call
     * @dev Use allowTransient for gas-efficient temporary access
     */
    function correctCrossContractCall(
        address processor
    ) external returns (bool) {
        // ✅ Grant temporary permission (expires at end of transaction)
        FHE.allowTransient(_secretValue, processor);

        // Now processor can use _secretValue in this transaction
        (bool success, ) = processor.call(
            abi.encodeWithSignature("process(uint256)", _secretValue)
        );

        return success;
    }

    /// @notice Helper to get balance for testing
    function getBalance(address user) external view returns (euint32) {
        return _balances[user];
    }
}

Last updated