GitHub Logo HIP-1195: Hiero hooks and an application to allowances

Author Michael Tinker
Working Group Richard Bair, Leemon Baird, Jasper Potts, Atul Mahamuni, Matthew DeLorenzo, Giuseppe Bertone, Greg Scullard, Steven Sheehy
Requested By Hashgraph
Discussions-To https://github.com/hiero-ledger/hiero-improvement-proposals/discussions/1172
Status Last Call
Review period ends Fri, 06 Jun 2025 07:00:00 +0000
Type Standards Track
Category Service
Created 2025-02-19
Updated 2025-05-23

Abstract

We propose hooks, programmable Hiero extension points that let users customize the behavior of their entities. In principle, hooks could be programmed in any language, but we begin with EVM hooks. Users program EVM hooks by writing contracts in a language like Solidity that compiles to EVM bytecode. EVM hooks are either pure (using neither storage nor external contracts); or lambdas (like code running in a cloud’s event-driven compute offering, which may access a database to use state or call other external services). Users can install many hooks at different indexes on the same entity. There is no limit on the number of hooks that can be installed on an entity, but the storage footprint, and hence rent, for that entity, will increase with the number of installed hooks.

As a first Hiero extension point, we propose allowance hooks. Users can install these hooks on their accounts. A Hiero API (HAPI) CryptoTransfer transaction can then reference a hook allowance just as it does an ERC-style allowance defined in HIP-376. The network uses the hook by calling its EVM bytecode at a specific function signature, passing in the details of the transfers proposed in the transaction. If the hook returns true, the network continues executing the transfer; otherwise the network rejects the transfer. Installing a hook on an account is analogous to adding a function to a smart contract; the hook executes with the account’s privileges when calling Hedera system contracts, just as a smart contract’s functions do.

Unlike smart contracts, which must encapsulate trust guarantees for multiple parties, lambdas belong to a single owner who can directly update their storage via a new LambdaSStore transaction that acts on EVM key-value pairs. This permits fast, inexpensive, arbitrary adjustments to a lambda’s behavior with less overhead than a typical ConsensusSubmitMessage; and far less overhead than a ContractCall.

Motivation

Hedera users often want to customize native entities instead of migrating their decentralized applications (dApps) to purely EVM-based smart contracts. Consider the following examples:

Hooks provide a more general solution to the problem of users needing custom business logic for their entities. For example, a token issuer might need to enforce rules on every transaction involving their token for regulatory or business reasons. A transfer hook installable on token types could enforce these rules without requiring the issuer to take a full design proposal through the HIP process. It would also preserve the performance and simplicity of the native APIs, unlike moving everything into custom ERC-20 smart contracts.

In short, by avoiding protocol-level changes for every important customization, hooks can greatly streamline innovation on a Hiero network while maintaining the performance and integrity of the native services.

Specification

First we specify how a Hiero network will charge, throttle, and execute EVM hooks. The execution section details how the EVM transaction for a hook differs from the EVM transaction of a top-level contract call. (Non-EVM hook programming models would need their own specifications.)

The protobuf API for hooks in general, and EVM hooks in particular, follows in later sections.

Gas charging

A primary concern for EVM hooks is deciding what account pays for the EVM gas upfront before executing the hook. We propose a simple, unified approach that hooks themselves can optimize with refund logic if desired. That is, for every hook,

  • The payer of the transaction triggering the hook pays for the upfront gas cost.
  • The hook API includes the gas fee charged as an API parameter, so the hook can refund some or all of this gas cost.
  • The payer will only be charged if they explicitly set a non-zero gas limit on the transaction.

We propose the same gas price for EVM hook execution as for other contract operations. However, unlike contract calls, which are charged purely in gas, hook executions are already “gated” by the fee of their triggering transaction. So it makes sense to reduce the intrinsic gas cost of their execution. We propose adding two more properties to give this effect while keeping it customizable for Hiero network operators.

hooks.evm.pureIntrinsicGasCost=1000
hooks.evm.lambdaIntrinsicGasCost=1000

Rent collection

An account’s storage footprint in network state grows with its number of associated tokens, and a contract’s storage footprint grows with its number of used storage slots. In the same way, an entity’s storage footprint will grow with both its number of installed hooks; and the number of storage slots used by those hooks.

At the time of this HIP, rent was not yet enabled on Hedera mainnet, but it is inevitable it will be in the future. To support seamless extension of rent to hooks, we propose to keep in network state two pieces of summary data for each entity that installs hooks:

  1. The number of hooks installed on the entity.
  2. The total number of storage slots used by the entity’s hooks.

The rent of an entity with N installed hooks using S storage slots will then scale linearly with N and S.

Throttling

We propose EVM hooks be subject to the same gas throttle as top-level contract calls. Specifically, when an EVM hook executes, its initial EVM sender address is the payer of the referencing transaction. If this payer is throttle exempt (that is, a system account with entity number below accounts.lastThrottleExempt=100), no throttles are applied. Otherwise, if the network is at capacity for gas usage, EVM hook execution will be throttled on that basis and the triggering transaction fail with a status of CONSENSUS_GAS_EXHAUSTED, just as a top-level contract call would.

The network will also throttle the rate at which hooks can be installed, using the same throttle buckets as for contract creation, with pricing similar to the current $1.00 USD cost of the HAPI contract create operation. (Or likely higher, to amortize the extra complexity of the hook lifecycle.) The dApps that build valuable meta-protocols using hooks may subsidize this cost to ease user onboarding.

The EVM environment for hooks

There are two important differences between the EVM execution environment for a hook installed on an entity and a top-level contract call. Namely,

  1. Throughout the hook’s EVM transaction, its bytecode always has the special address 0x16d.
  2. The hook is an extension of the installing entity; it has the installer’s Hiero system contract privileges.

Everything else is identical to the EVM environment of a top-level contract call in the same block.

Important: We recognize, and strongly affirm, the sensitivity and power of making lambda hooks an extension of the installing entity. Users with material security hygiene will not casually draft or install hooks on their entities. The hooks in broad use will have been published as application HIPs; been extensively reviewed, debated, and audited; and ultimately given special treatment by ecosystem wallets and block explorers.

Let us consider the two differences above in more detail.

0x16d contract address

Conceptually, the initial EVM message frame for a hook transaction is a DELEGATECALL from the system contract address 0x16d to the hook’s implementing contract. That is, even though the hook’s implementing bytecode was created with a Hiero contract id 0.0.H, that bytecode executes with a contract address of 0x16d. If it calls another contract and that contract, in turn, calls back to address 0x16d, then control flow returns to the hook’s bytecode.

As a concrete example, suppose account 0.0.A installs an EVM lambda hook for the ACCOUNT_ALLOWANCE_HOOK extension point at index 1. The hook’s implementing contract is 0.0.H with EVM address 0xab...cd. Now 0.0.B with EVM address 0x01...23 sends a CryptoTransfer transaction that references the hook 0.0.A/1 with gas limit 100_000.

The network will construct an initial EVM message frame with,

  1. sender address 0x01...23;
  2. receiver address 0x16d;
  3. contract address 0xab...cd (hence the source of the executing bytecode);
  4. Storage of the 0.0.A/1 lambda EVM hook; and,
  5. Gas remaining of 99_000 (lower intrinsic gas cost).

The hook can then proceed with arbitrary actions, including calls to other contracts, SLOAD and SSTORE operations with its storage, and so on. We expect the most common type of hook contract to implement a single external method that reverts when not executed by the network as a hook. That is,

/// The interface for a generic EVM hook.
interface IHieroHook {
    /// The context the hook is executing in
    struct HookContext {
        /// The address of the installing entity the hook is executing on behalf of
        address installer;
        /// The fee the transaction payer was charged for the triggering transaction
        uint256 txnFee;
        /// The gas cost the transaction payer was charged for specifically this hook
        uint256 gasCost;
        /// The memo of the triggering transaction
        string memo;
        /// The "extended" call data passed to the hook
        bytes args;
    }
}

contract HookContract {
    function hookFunction(IHieroHook.HookContext calldata context) external payable {
        // Revert if we are not executing as a hook
        require(address(this) == 0x16d, "Contract can only be called as a hook");
        // Continue executing as a hook on behalf of the installer address
    }
}

(Note that the hookFunction(IHieroHook.HookContext) signature above is just a representative placeholder. Each actual hook, such as the account allowance hook in this HIP, will have its own ABI signature that adds relevant parameters in addition to the universal context parameter.)

Privileges of the installing entity

A hook is an extension of the installing entity, in the style of Ethereum’s account abstraction vision. This means the hook has the Hiero privileges of the installer. If it calls Hiero system contracts, they automatically execute on behalf of the installing entity.

And if the installing entity has a balance, the hook can use the installer’s balance to refund gas fees to msg.sender or otherwise transfer value.

Again, we realize and affirm that programmatic entity extension introduces security risks. But users that install only the hooks that the community has audited and adopted through application HIPs can then enjoy the full power of EVM programmability without incurring any special risk.

Pure EVM hooks

Pure EVM hooks execute in the same conceptual model, but in an extremely restricted mode. Not only is the initial EVM frame marked static, which prohibits all state-changing operations; but the network also disables the PREVRANDAO, SLOAD, and CALL opcodes. That is, a pure hook cannot do anything but apply a pure function to its input data.

Mirror node and block explorer support

Mirror nodes and block explorers will want to give at least summary data of what hooks are installed on an entity. For example, the Hedera public mirror node has an /api/v1/accounts/{accountId} endpoint that returns a JSON object with the account’s current balance, keys, and so on. We propose this JSON object be extended with fields number_installed_hooks and total_hook_storage_slots to give the number of installed hooks and the total number of storage slots used by those hooks for the account.

It would also be natural to add a new /api/v1/accounts/{accountId}/hooks endpoint that returns a paged JSON object with at least a subset of the information in the example below.

{
  "hooks": [
    {
      /* Composite identifier: {installer_id}/{index} */
      "hook_id": "0.0.123/1",

      /* The entity that owns (installed) the hook */
      "installer_id": "0.0.123",

      /* Position in the installer’s doubly-linked hook list */
      "index": 1,

      /* Extension point implemented by this hook */
      "extension_point": "ACCOUNT_ALLOWANCE_HOOK",

      /* PURE | LAMBDA */
      "type": "LAMBDA",

      /* Contract that contains the executing byte-code */
      "hook_contract_id": "0.0.456",

      /* Storage utilisation summary (for rent & UI hints) */
      "num_storage_slots": 12,

      /* Lifecycle metadata */
      "created_timestamp": "1726874345.123456789",
      "deleted": false,

      /* Optional storage key that can manage the hook's EVM storage */
      "storage_key": {
        "_type": "ED25519",
        "key": "0x302a300506032b6570032100e5b2…"
      },

      /* Convenience links into other Mirror resources */
      "links": {
        "self": "/api/v1/accounts/0.0.123/hooks/1",
        "contract": "/api/v1/contracts/0.0.456",
        "storage": "/api/v1/accounts/0.0.123/hooks/1/storage"
      }
    }
  ],

  /* Standard Mirror-paging wrapper */
  "links": {
    "next": null
  }
}

Core HAPI protobufs

A hook’s extension point is one of an enumeration which now includes only the account allowance hook,

/***
 * The Hiero extension points that accept a hook.
 */
enum HookExtensionPoint {
  /**
   * Used to customize an account's allowances during a CryptoTransfer transaction.
   */
  ACCOUNT_ALLOWANCE_HOOK = 0;
}

Users install hooks by setting new HookInstall fields on CryptoCreates or CryptoUpdates transaction. This message is,

/***
 * How to install a hook.
 */
message HookInstall {
  /**
   * The extension point for the hook.
   */
  HookExtensionPoint extension_point = 1;

  /**
   * The entity index to install the hook at.
   */
  uint64 index = 2;

  /**
   * The hook implementation.
   */
  oneof hook {
    /**
     * A hook programmed in EVM bytecode that does not require access to state
     * or interactions with external contracts.
     */
    PureEvmHook pure_evm_hook = 3;
    /**
     * A hook programmed in EVM bytecode that may access state or interact with
     * external contracts.
     */
    LambdaEvmHook lambda_evm_hook = 4;
  }
}

The PureEvmHook and LambdaEvmHook messages share a common EvmHookSpec message that specifies the source of the hook’s EVM bytecode. The LambdaEvmHook message also includes the initial storage slots for a lambda hook, if desired.

/**
 * Definition of a pure EVM hook.
 */
message PureEvmHook {
  /**
   * The specification for the hook.
   */
  EvmHookSpec spec = 1;
}

/**
 * Definition of a lambda EVM hook.
 */
message LambdaEvmHook {
  /**
   * The specification for the hook.
   */
  EvmHookSpec spec = 1;

  /**
   * Initial storage contents for the lambda, if any.
   */
  repeated LambdaStorageSlot storage_slots = 2;
  
  /**
   * If set, a key that that can be used to sign LambdaSStore transactions
   * customizing the lambda's storage. If not set, only the installer's key
   * can authorize a LambdaSStore transaction.
   */
  proto.Key storage_key = 3;
}

/**
 * Shared specifications for an EVM hook. May be used for any extension point.
 */
message EvmHookSpec {
  /**
   * The source of the EVM bytecode for the hook.
   */
  oneof bytecode_source {
    /**
     * The id of a contract that implements the extension point API with EVM bytecode.
     */
    ContractID contract_id = 1;
  }
}

/**
 * A slot in the storage of a lambda EVM hook.
 */
message LambdaStorageSlot {
  /**
   * The 32-byte key of the slot; leading zeros may be omitted.
   */
  bytes key = 1;

  /**
   * If the slot is present and non-zero, the 32-byte value of the slot;
   * leaving this field empty or setting it to binary zeros in an update
   * removes the slot.
   */
  bytes value = 2;
}

The indexes of newly installed hooks will appear in the legacy TransactionReceipt if records streams are enabled,


message TransactionReceipt {
  // ...

  /**
   * In the receipt of a successful create or update transaction for an entity that supports hooks,
   * the indexes of any newly installed hooks.
   */
  repeated uint64 installed_hook_indexes = 16;
}

Once a hook is installed to an entity, a transaction generally references it by index relative to an implicit owner. The details of the call are specified based on its type; for example, EVM hook calls are specified by an EvmHookCall message that gives optional call data and gas limit.

If the called hook does not match the given call specification, the network will fail the transaction with BAD_HOOK_REQUEST. If there is no hook installed at the specified index, the network will fail the transaction with HOOK_NOT_FOUND.

/**
 * Specifies a call to a hook from within a transaction where
 * the hook owner is implied by the point of use. (For example,
 * it would never make sense to try to use an account allowance
 * hook for account 0.0.X inside an AccountAmount for account
 * 0.0.Y; hence we only need to give the index of which of
 * 0.0.Y's hooks we want to call.)
 */
message HookCall {
  /**
   * The index of the hook to call.
   */
  uint64 index = 1;

  /**
   * Specifies details of the call.
   */
  oneof call_spec {
    /**
     * Specification of how to call an EVM hook.
     */
    EvmHookCall evm_hook_call = 2;
  }
}

/**
 * Specifies details of a call to an EVM hook.
 */
message EvmHookCall {
  /**
   * Extended call data to pass to the hook as a bytes args.
   */
  bytes extended_call_data = 1;

  /**
   * The gas limit to use.
   */
  uint64 gas_limit = 2;
}

Core system protobufs

Once a hook is installed, it has an id in the network state.

/**
 * Once a hook is installed, its id.
 */
message HookId {
  /**
   * The id of the hook's installer.
   */
  HookInstallerId installer_id = 1;

  /**
   * A unique identifier for the hook given the installer.
   */
  uint64 index = 2;
}

/**
 * The id of an entity that has installed a hook.
 */
message HookInstallerId {
  oneof installer_id {
    /**
     * An account installing a hook.
     */
    AccountID account_id = 1;
  }
}

EVM hooks will be implemented by internal dispatch from each installing entity type’s service to the ContractService. (A hook with a different programming model would require very different implementation details, so we restrict our attention to EVM hooks.)

The dispatch for installing, executing, and uninstalling EVM hooks is a new HookDispatchTransactionBody with a choice of three actions.

/**
 * Dispatches a hook action to an appropriate service.
 */
message HookDispatchTransactionBody {
  oneof action {
    /**
     * The id of the hook to uninstall.
     */
    HookId hook_id_to_uninstall = 1;

    /**
     * The installation of a new hook.
     */
    HookInstallation installation = 2;

    /**
     * An execution of an installed hook.
     */
    HookExecution execution = 3;
  }
}

/**
 * Specifies the execution of a hook by its installer id and
 * the details of the call (which includes the index).
 */
message HookExecution {
  /**
   * The id of the hook's installer.
   */
  HookInstallerId installer_id = 1;

  /**
   * The call details.
   */
  HookCall call = 2;
}

Since a pure EVM hook by definition has no call trace or storage access, its execution has no footprint in the block stream. Executing a lambda EVM hook, however, produces ContractCall block items (EventTransaction, TransactionResult, TransactionOutput) as following children of the triggering transaction, in the order of each executed lambda.

When an EVM hook is installed, its representation in ContractService state is as below. (Note the prev/next pointers are an implementation detail transparent to the user. The protocol uses them to efficiently traverse the list of installed hooks when archiving the account.)

/**
 * The representation of a lambda in state, including the previous and next indexes of its owner's lambda list.
 */
message EvmHookState {
  /**
   * For state proofs, the id of this hook.
   */
  proto.HookId hook_id = 1;

  /**
   * The type of the hook.
   */
  EvmHookType type = 2;

  /**
   * The type of the extension point the hook implements.
   */
  proto.HookExtensionPoint extension_point = 3;

  /**
   * The id of the contract with this hook's bytecode.
   */
  proto.ContractID hook_contract_id = 4;

  /**
   * True if the hook has been removed.
   */
  bool deleted = 5;

  /**
   * For a lambda EVM hook, its first storage key.
   */
  bytes first_contract_storage_key = 6;

  /**
   * If non-zero, the index of the hook preceding this one in the owner's
   * doubly-linked list of hook.
   */
  uint64 previous_index = 7;

  /**
   * If non-zero, the index of the hook following this one in the owner's
   * doubly-linked list of hooks.
   */
  uint64 next_index = 8;

  /**
   * The number of storage slots a lambda EVM hook is using.
   */
  uint64 num_storage_slots = 9;
}

/**
 * The type of an EVM hook.
 */
enum EvmHookType {
  /**
   * A pure EVM hook.
   */
  PURE = 0;
  /**
   * A lambda EVM hook.
   */
  LAMBDA = 1;
}

And its storage is keyed by the following type,

/**
 * The key of a lambda's storage slot.
 *
 * For each lambda, its EVM storage is a mapping of 256-bit keys (or "words")
 * to 256-bit values.
 */
message LambdaSlotKey {
  /**
   * The id of the lambda EVM hook that owns this slot.
   */
  proto.HookId hook_id = 1;

  /**
   * The EVM key of this slot, left-padded with zeros to form a 256-bit word.
   */
  bytes key = 2;
}

After an entity has installed a lambda, a new LambdaSStore transaction supports efficiently updating the lambda’s storage. It must be signed by either the installer’s key or the storage key set in the lambda’s LambdaEvmHook definition.

/**
 * Adds or removes key/value pairs in the storage of a lambda. Either the installer's key or an storage key defined in 
 * the `LambdaEvmHook` must sign the transaction.
 */
message LambdaSStoreTransactionBody {
  /**
   * The id of the lambda EVM hook whose storage is being updated.
   */
  proto.HookId hook_id = 1;

  /**
   * The updates to the storage of the lambda.
   */
  repeated LambdaStorageSlot storage_slots = 2;
}

Account allowance HAPI protobufs

The account allowance extension point is the only extension point defined in this HIP. Hooks for this extension are installed on an account via either a CryptoCreate or CryptoUpdate transaction. That is, we extend the CryptoCreateTransactionBody with a hook_installs field, and the CryptoUpdateTransactionBody with fields to install and uninstall hooks.

message CryptoCreateTransactionBody {
  // ...

  /**
   * The hook installs to run immediately after creating this account.
   */
  repeated HookInstall hook_installs = 19;
}

message CryptoUpdateTransactionBody {
  // ...

  /**
   * The indexes of the hooks to uninstall from the account, before executing any installs.
   */
  repeated uint64 hook_indexes_to_uninstall = 19;
  /**
   * The hooks to install on the account.
   */
  repeated HookInstall hook_installs = 20;
}

If either transaction repeats a hook index in its hook_installs list, it will fail with status HOOK_INDEX_REPEATED_IN_INSTALLS. If the CryptoUpdateTransactionBody tries to install a hook at an index that is already occupied, it will fail with status HOOK_INDEX_IN_USE. If the CryptoUpdateTransactionBody tries to uninstall a hook from an index not in use, it will fail with status HOOK_NOT_FOUND. To support atomic hook updates for compliance reasons, we do support uninstalling and reinstalling a hook at the same index in a single CryptoUpdate.

We extend the Account message in TokenService state to include the number of installed hooks and the total number of storage slots they use; as well as the index of the last hooks in the doubly-linked list of hook installed by the account.

message Account {
  // ...
  /**
   * The number of hook currently installed on this account.
   */
  uint64 number_installed_hooks = 36;
  
   /**
    * The total number of storage slots used by all hooks installed on this account.
    */
   uint64 total_hook_storage_slots = 37;

  /**
   * If positive, the index of the first hook installed on this account.
   */
  uint64 first_hook_index = 38;
}

For a successful such CryptoCreate or CryptoUpdate, the indexes of the newly installed hooks will appear in the legacy record TransactionReceipt if record streams are still enabled.

Now we need to let a CryptoTransfer reference such a hook. For this we extend the AccountAmount and NftTransfer messages used in the CryptoTransferTransactionBody.

message AccountAmount {
  // ...
  /**
   * If set, a call to a hook of type `ACCOUNT_ALLOWANCE_HOOK` installed on
   * accountID that must succeed for the transaction to occur.
   */
  HookCall allowance_hook_call = 4;
}

message NftTransfer {
  // ...
  /**
   * If set, a call to a hook of type `ACCOUNT_ALLOWANCE_HOOK` installed on
   * senderAccountID that must succeed for the transaction to occur.
   */
  HookCall sender_allowance_hook_call = 5;

  /**
   * If set, a call to a hook of type `ACCOUNT_ALLOWANCE_HOOK` installed on
   * receiverAccountID that must succeed for the transaction to occur.
   */
  HookCall receiver_allowance_hook_call = 6;
}

Note that NftTransfer supports both sender and receiver transfer allowance hooks, since the transaction may need to use the receiver hook to satisfy a receiver_sig_required=true setting.

The transfer allowance ABI

The account allowance EVM hook ABI is as follows,

// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.4.9 <0.9.0;
pragma experimental ABIEncoderV2;

import './IHederaTokenService.sol';

/// The interface for a generic EVM hook.
interface IHieroHook {
    /// The context the hook is executing in
    struct HookContext {
        /// The address of the installing entity the hook is executing on behalf of
        address installer;
        /// The fee the transaction payer was charged for the triggering transaction
        uint256 txnFee;
        /// The gas cost the transaction payer was charged for specifically this hook
        uint256 gasCost;
        /// The memo of the triggering transaction
        string memo;
        /// The "extended" call data passed to the hook
        bytes args;
    }
}

/// The interface for an account allowance hook.
interface IHieroAccountAllowanceHook {
    /// Combines HBAR and HTS asset transfers.
    struct Transfers {
        /// The HBAR transfers
        IHederaTokenService.TransferList hbar;
        /// The HTS token transfers
        IHederaTokenService.TokenTransferList[] tokens;
    }

    /// Combines the full proposed transfers for a Hiero transaction,
    /// including both its direct transfers and the implied HIP-18
    /// custom fee transfers.
    struct ProposedTransfers {
        /// The transaction's direct transfers
        Transfers direct;
        /// The transaction's assessed custom fees
        Transfers customFee;
    }

    /// Decides if the proposed transfers are allowed, optionally in
    /// the presence of additional context encoded by the transaction
    /// payer in the extra args.
    /// @param context The context of the hook call
    /// @param proposedTransfers The proposed transfers
    /// @return true If the proposed transfers are allowed, false or revert otherwise
    function allow(
       IHieroHook.HookContext calldata context,
       ProposedTransfers memory proposedTransfers
    ) external payable returns (bool);
}

Examples

Next we provide two examples of account allowance EVM hooks.

One-time passcode allowances

An NFT project prides itself on having only the very cleverest holders. They distribute their collection by daily sending a NFT from the treasury to account 0.0.X, and publishing a puzzle. The answer to the puzzle is a one-time use passcode that allows the solver to collect the NFT.

In particular, the project team installs on account 0.0.X at index 1 an account allowance lambda EVM hook that references a contract created as below.

import "./IHieroAccountAllowanceHook.sol";

contract OneTimeCodeTransferAllowance is IHieroAccountAllowanceHook {
    /// The hash of a one-time use passcode string, at storage slot 0x00
    bytes32 passcodeHash;

    /// Allow the proposed transfers if and only if the args are the
    /// ABI encoding of the current one-time use passcode in storage.
    ///
    /// NOTE: this lambda's behavior does not depend on the installer address,
    /// only the contents of the installed lambda's 0x00 storage slot
    function allow(
        IHieroHook.HookContext calldata context,
        ProposedTransfers memory proposedTransfers
    ) external override payable returns (bool) {
        require(address(this) == 0x16d, "Contract can only be called as a hook");
        (string memory passcode) = abi.decode(context.args, (string));
        bytes32 hash = keccak256(abi.encodePacked(passcode));
        bool matches = hash == passcodeHash;
        if (matches) {
            passcodeHash = 0;
        }
        return matches;
    }
}

As great aficionados of the project, we see one day that 0.0.X holds our favorite NFT of all, serial 123; and that a LambdaSStore from 0.0.X set the storage slot with key 0x00 to the hash 0xc7eba0ccc01e89eb5c2f8e450b820ee9bb6af63e812f7ea12681cfdc454c4687. We rush to solve the puzzle, and deduce the passcode is the string, "These violent delights have violent ends". Now we can transfer the NFT to our account 0.0.U by submitting a CryptoTransfer with,

NftTransfer {
  senderAccountID: 0.0.X
  receiverAccountID: 0.0.U
  serialNumber: 123
  sender_allowance_hook_call: HookCall {
    index: 1
    evm_hook_call: EvmHookCall {
      extended_call_data: "These violent delights have violent ends"
    }
  }
}

Compare this example to the pure smart contract approach, where the project would need to write a more complex smart contract that is aware of what serial number it currently holds; and makes calls to the HTS system contract to distribute NFTs. Instead of the team using LambdaSStore to update the passcode with less overhead and cost to the network than even a ConsensusSubmitMessage, they would need to submit a ContractCall. Instead of us using a CryptoTransfer to collect our prize with maximum legibility and minimum cost, we would also need to submit a ContractCall to the project’s smart contract with a significantly higher gas limit.

For a trivial example like this, the cost and efficiency deltas may not seem decisive (unless the project was running a very large number of these puzzles). But the idea of releasing contracts from the burden of duplicating native protocol logic is deceptively powerful. The cost and efficiency savings for a complex dApp could be enormous, unlocking entire new classes of applications that would be impractical to build on Hedera today.

Receiver signature waiver for HTS assets without custom fees

In this example we have our own account 0.0.Y with receiver_sig_required=true, and want to carve out an exception for exactly HTS token credits to our account with no assessed custom fees. We install a pure EVM hook at index 2 whose referenced contract is as follows,

import "./IHederaTokenService.sol";
import "./IHieroAccountAllowanceHook.sol";

contract CreditSansCustomFeesTokenAllowance is IHieroAccountAllowanceHook {
    /// Allows the proposed transfers only if,
    ///   (1) The only transfers are direct HTS asset transfers
    ///   (2) The installer is not debited
    ///   (3) The installer is credited
    function allow(
        IHieroHook.HookContext calldata context,
        ProposedTransfers memory proposedTransfers
    ) external override view returns (bool) {
        require(address(this) == 0x16d, "Contract can only be called as a hook");
        if (proposedTransfers.direct.hbar.transfers.length > 0
                || proposedTransfers.customFee.hbar.transfers.length > 0
                || proposedTransfers.customFee.tokens.length > 0) {
            return false;
        }
        bool installerCredited = false;
        address installer = context.installer;
        for (uint256 i = 0; i < proposedTransfers.tokens.length; i++) {
            IHederaTokenService.AccountAmount[] memory transfers = proposedTransfers.tokens[i].transfers;
            for (uint256 j = 0; j < transfers.length; j++) {
                if (transfers[j].accountID == installer) {
                    if (transfers[j].amount < 0) {
                        return false;
                    } else if (transfers[j].amount > 0) {
                        installerCredited = true;
                    }
                }
            }
            IHederaTokenService.NftTransfer[] memory nftTransfers = proposedTransfers.tokens[i].nftTransfers;
            for (uint256 j = 0; j < nftTransfers.length; j++) {
                if (nftTransfers[j].senderAccountID == installer) {
                    return false;
                } else if (nftTransfers[j].receiverAccountID == installer) {
                    installerCredited = true;
                }
            }
        }
        return installerCredited;
    }
}

Backwards Compatibility

This HIP adds a net new feature to the protocol. Any account that does not install a hook will see identical behavior in all circumstances. Any payer account that does explicitly set a gas limit to cover a hook’s execution will be at no risk of having a hook execute.

Security Implications

Since EVM hook executions are subject to the same gas charges and throttles as normal contract executions; and hook installations are subject to the same throttles as contract creations, we do not expect this HIP to introduce any new denial of service vector.

The main security concerns with account allowance hooks are the same as with smart contracts. That is,

  • A hook author could code a bug allowing an attacker to exploit the hook.
  • A malicious dApp could trick a user into installing a hook with a backdoor for the dApp author to exploit.

Hook authors must mitigate the risk of bugs by rigorous testing and code review. Users must remain vigilant about signing transactions from dApps of questionable integrity. As reiterated above, we recommend and expect that hooks with broad usage will be published as application HIPs, and that users will adopt them only as wallets and block explorers give full support and visibility into their semantics.

Reference Implementation

In progress, please see here.

Rejected Ideas

  1. We considered automatic hooks that execute even without being explicitly referenced by a transaction. While this feature could be useful in the future (for example, as an “anti-dusting” hook), we deemed it out of scope here.
  2. We considered adding an IHieroExecutionEnv interface to the 0x16d system contract with APIs available only to executing EVM hooks. While interesting, there was no benefit for the initial use case in this HIP.
  3. We considered using a family of allowance extension points, one for each type of asset exchange. (That is, PRE_HBAR_DEBIT, PRE_FUNGIBLE_CREDIT, PRE_NFT_TRANSFER, and so on.) Ultimately the single ACCOUNT_ALLOWANCE extension point seemed more approachable, especially as calls can encode any extra context the hook’s allow() method needs to efficiently focus on one aspect of the proposed transfers.
  4. We considered support multiple charging schemes for hooks, such as CALLER_PAYS and CALLER_PAYS_ON_FAILURE. Ultimately it seemed better to keep the charging scheme simple and let hooks manage any refunds themselves.

Open Issues

No known open issues.

References

Copyright/license

This document is licensed under the Apache License, Version 2.0 – see LICENSE or (https://www.apache.org/licenses/LICENSE-2.0)

Citation

Please cite this document as: