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 Approved
Needs Hedera Review Yes
Needs Hiero Approval Yes
Last Call Period Ends Fri, 06 Jun 2025 07:00:00 +0000
Type Standards Track
Category Service
Created 2025-02-19
Updated 2025-07-11

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 event-driven code running in the cloud, that may keep state in a database to use state or call other external services). For any given entity, its user can create many hooks for that entity with different 64-bit hook ids. There is no limit on the number of hook ids than an entity can use; but its storage footprint, and hence rent, will increase proportionally.

As a first Hiero extension point, we propose account allowance hooks. Users can create these hooks on their accounts, and a Hiero API (HAPI) CryptoTransfer transaction can then reference an allowance hook just as it does an ERC-style allowance defined in HIP-376. The network uses the allowance hook by calling its EVM bytecode at a well-known function signature, passing in the context of the triggering CryptoTransfer. If the hook function returns true, the network continues executing the transfer; otherwise the network rejects the transfer. Creating an account hook is analogous to adding a function to a smart contract: That is, 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, lambda hooks belong to a single owner who can directly update their storage via a new LambdaSStore transaction that acts on EVM storage slots. This permits fast, heap 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) exclusively to EVM 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 for token entities 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.

EVM hook 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 in new HIPs.)

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 never be charged for more gas than a gas limit they set at the hook reference point.

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 its number of 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 using hooks:

  1. The number of hooks the entity has; and,
  2. The total number of storage slots used by the entity’s lambdas.

The rent of an entity with N 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 created, 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 metaprotocols 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 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 its entity; it has that entity’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 an entity’s hooks an extension of the entity itself. Users with security hygiene will not casually draft and use hooks. The hooks in broad use will have been published as application HIPs that specify valuable metaprotocols; and will have been extensively reviewed, debated, and audited; and ultimately given special treatment by ecosystem wallets and block explorers.

We now examine the two differences in the EVM hook execution environment 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 a user creates a ACCOUNT_ALLOWANCE_HOOK lambda with hook id 1 for account 0.0.A. 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; and,
  2. receiver address 0x16d; and,
  3. contract address 0xab...cd (hence the source of the executing bytecode); and,
  4. Storage of the 0.0.A/1 lambda; and,
  5. Gas remaining of 99_000 (lower intrinsic gas cost).

The lambda 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 EVM hook contract to implement a single external method that reverts when not executed by the network as a hook. That is,

/// The interface for an EVM hook.
interface IHieroHook {
    /// The context the hook is executing in
    struct HookContext {
        /// The address of the entity the hook is executing on behalf of
        address owner;
        /// 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;
        /// Any extra call data passed to the hook
        bytes data;
    }
}

/// A contract serving as an EVM hook.
contract HookContract {
    /// A single external function to be executed as a hook.
    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 owner
    }
}

(Note that the hookFunction(IHieroHook.HookContext) signature above is just a representative placeholder. Although the IHieroHook.HookContext context parameter will be universal to all hooks, many hooks—such as the ACCOUNT_ALLOWANCE hook in this HIP—will specify more parameters.)

Privileges of the owner

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

And if the owning entity has a balance, the hook can use that balance to transfer value; for example, to refund gas fees to msg.sender.

Once again, we realize and affirm that programmatic entity extension introduces security risks. But users that limit themselves to 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 an entity’s hooks. 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_hooks and total_lambda_storage_slots to give the number of account’s hooks and the total number of storage slots used by its lambda.

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": [
    {
      /* The entity that owns the hook */
      "owner_id": "0.0.123",
      
      /* May be composed with owner id: {owner_id}/{hook_id} */
      "hook_id": "1",

      /* Extension point this hook implements */
      "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 node 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 node paging wrapper */
  "links": {
    "next": null
  }
}

Core HAPI protobufs

A hook’s extension point is one of an enumeration which will first be just the account allowance hook in this HIP.

/***
 * 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 create hooks by setting new HookCreationDetails fields on CryptoCreate, CryptoUpdate, ContractCreate, or ContractUpdate transactions. This message is,

/***
 * The details of a hook's creation.
 */
message HookCreationDetails {
  /**
   * The extension point for the hook.
   */
  HookExtensionPoint extension_point = 1;

  /**
   * The id to create the hook at.
   */
  uint64 hook_id = 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;
  }

  /**
   * If set, a key that that can be used to remove or replace the hook; or (if
   * applicable, as with a lambda EVM hook) perform transactions that customize
   * the hook.
   */
  proto.Key admin_key = 5;
}

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 updates for the lambda, if any.
   */
  repeated LambdaStorageUpdate storage_updates = 2;
}

/**
 * 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;
  }
}

/**
 * Specifies a key/value pair in the storage of a lambda, either by the explicit storage
 * slot contents; or by a combination of a Solidity mapping's slot key and the key into
 * that mapping.
 */
message LambdaStorageUpdate {
  oneof update {
    /**
     * An explicit storage slot update.
     */
    LambdaStorageSlot storage_slot = 1;
    /**
     * An implicit storage slot update specified as a Solidity mapping entry.
     */
    LambdaMappingEntry mapping_entry = 2;
  }
}

/**
 * Specifies a storage slot update via indirection into a Solidity mapping.
 * <p>
 * Concretely, if the Solidity mapping is itself at slot `mapping_slot`, then
 * the * storage slot for key `key` in the mapping is defined by the relationship
 * `key_storage_slot = keccak256(abi.encodePacked(mapping_slot, key))`.
 * <p>
 * This message lets a metaprotocol be specified in terms of changes to a
 * Solidity mapping's entries. If only raw slots could be updated, then a block
 * stream consumer following the metaprotocol would have to invert the Keccak256
 * hash to determine which mapping entry was being updated, which is not possible.
 */
message LambdaMappingEntry {
  /**
   * The slot that corresponds to the Solidity mapping itself.
   */
  bytes mapping_slot = 1;

  /**
   * The 32-byte key of the mapping entry; leading zeros may be omitted.
   */
  bytes key = 2;

  /**
   * If the mapping entry is present and non-zero, the 32-byte value of key in the mapping;
   * leading zeros may be omitted. Leaving this field empty or setting it to binary zeros
   * in an update clears the entry from the mapping.
   */
  bytes value = 3;
}

/**
 * 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;
}

Once a hook is created for an entity, a transaction references the hook by its id relative to an implicit owner. Additional details for the hook’s execution may be given as well; for example, EVM hook calls are specified by an EvmHookCall message that gives extra call data and gas limit.

If the called hook does not match the provided specification, the network will fail the transaction with BAD_HOOK_REQUEST. If the relevant entity has no hook with the given id, 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.
 * <p>
 * For example, if the hook is an account allowance hook, then it is clear from the balance adjustment being
 * attempted which account must own the referenced hook. 
 */
message HookCall {
  /**
   * The id of the hook to call.
   */
  uint64 hook_id = 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 {
  /**
   * Call data to pass to the hook via the IHieroHook.HookContext#data field.
   */
  bytes data = 1;

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

Core system protobufs

Once a hook is created, it has a unique composite id in the network state. The two components are the id of its owning entity and a hook id. The hook id is an arbitrary 64-bit value that need not be sequential relative to other hooks owned by the entity. However, an entity may only have one hook with a certain id at a time.

/**
 * Once a hook is created, its full id.
 * <p>
 * A composite of its owning entity's id and an arbitrary 64-bit hook id (which need not be sequential).
 */
message CreatedHookId {
  /**
   * The hook's owning entity id.
   */
  HookEntityId entity_id = 1;

  /**
   * An arbitrary 64-bit identifier.
   */
  int64 hook_id = 2;
}

/**
 * The id of an entity using a hook.
 */
message HookEntityId {
  oneof entity_id {
    /**
     * An account using a hook.
     */
    AccountID account_id = 1;
  }
}

EVM hooks will be implemented by internal dispatch from each owning 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 creating, deleting, and executing, 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 delete.
     */
    CreatedHookId hook_id_to_delete = 1;

    /**
     * A new hook creation.
     */
    HookCreation creation = 2;

    /**
     * Execution details of an existing hook.
     */
    HookExecution execution = 3;
  }
}

/**
 * Details the execution of a hook.
 */
message HookExecution {
  /**
   * The id of the hook's entity.
   */
  HookEntityId hook_entity_id = 1;

  /**
   * The details of the call, including which hook id to call.
   */
  HookCall call = 2;
}

Executing an EVM hook produces ContractCall block items (SignedTransation, TransactionResult, TransactionOutput, and possibly TraceData) as following synthetic children of the triggering transaction, in the order of each executed hook.

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

/**
 * The representation of an EVM hook in state, including any previous and next hook ids in its owner's list.
 */
message EvmHookState {
  /**
   * For state proofs, the id of this hook.
   */
  proto.CreatedHookId hook_id = 1;

  /**
   * The type of the EVM 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 deleted.
   */
  bool deleted = 5;

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

  /**
   * If set, the id of the hook preceding this one in the owner's
   * doubly-linked list of hooks.
   */
  google.protobuf.Int64Value previous_hook_id = 7;

  /**
   * If set, the id of the hook following this one in the owner's
   * doubly-linked list of hooks.
   */
  google.protobuf.Int64Value next_hook_id = 8;

  /**
   * The number of storage slots a lambda is using.
   */
  uint32 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.CreatedHookId hook_id = 1;

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

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

/**
 * Adds or removes key/value pairs in the storage of a lambda.
 * <p>
 * Either the lambda owner's key or the lambda's admin key must sign this transaction.
 */
message LambdaSStoreTransactionBody {
  /**
   * The id of the lambda whose storage is being updated.
   */
  CreatedHookId hook_id = 1;

  /**
   * The updates to the storage of the lambda.
   */
  repeated LambdaStorageUpdate storage_updates = 2;
}

Account allowance HAPI protobufs

The account allowance extension point is the only extension point defined in this HIP. Hooks of this type are created for an account via either a CryptoCreate or CryptoUpdate transaction; and for a contract account via either a ContractCreate or ContractUpdate transaction. A future HIP might add Hiero system contracts to let a contract manage its hooks programmatically, but that is outside our scope here. (Note this also means hooks cannot be managed via a JSON-RPC relay; only through native HAPI transactions.)

Here we simply extend the CryptoCreateTransactionBody and ContractCreateTransactionBody messages with a field to create initial hooks along with an account or contract; and the CryptoUpdateTransactionBody and ContractUpdateTransactionBody messages with fields to create and delete hooks from existing accounts and contracts.

message CryptoCreateTransactionBody {
  // ...

  /**
   * Details of hooks to add immediately after creating this account.
   */
  repeated HookCreationDetails hook_creation_details = 19;
}

message ContractCreateTransactionBody {
  // ...
  
  /**
   * Details of hooks to add immediately after creating this contract.
   */
  repeated HookCreationDetails hook_creation_details = 20;
}

message CryptoUpdateTransactionBody {
  // ...

  /**
   * The hooks to create for the account.
   */
  repeated HookCreationDetails hook_creation_details = 19;

  /**
   * The ids the hooks to delete from the account.
   */
  repeated int64 hook_ids_to_delete = 20;
}

message ContractUpdateTransactionBody {
  // ...
  
  /**
   * The hooks to create for the contract.
   */
  repeated HookCreationDetails hook_creation_details = 16;

  /**
   * The ids the hooks to delete from the contract.
   */
  repeated int64 hook_ids_to_delete = 20;
}

If any transaction repeats a hook id in its hook_creation_details list, it will fail with status HOOK_ID_REPEATED_IN_CREATION_DETAILS. If the CryptoUpdateTransactionBody or ContractUpdateTransactionBody tries to create a hook with an id that is already occupied, it will fail with status INDEX_IN_USE. Similarly, if either update transaction tries to delete a hook with an id not in use, it will fail with status HOOK_NOT_FOUND. To support atomic hook updates for compliance reasons, we do support deleting and recreating a hook with the same id in a single update transaction. (That is, the hook_ids_to_delete list is processed first; then the hook_creation_details list.)

We extend the Account message in TokenService state to include the number of hooks in use by an account or contract, and the total number of storage slots their lambdas use; as well as the id of the last hook in the doubly-linked list of the account or contract’s hooks.

message Account {
  // ...

  /**
   * The number of hooks currently in use on this account.
   */
  uint64 number_hooks_in_use = 36;

  /**
   * If the account has more than zero hooks in use, the id of the first hook in its
   * doubly-linked list of hooks.
   */
  int64 first_hook_id = 37;

  /**
   * The number of storage slots in use by this account's lambdas.
   */
  uint64 number_lambda_storage_slots = 38;
}

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` on scoped
   * account; the hook's invoked methods must not revert and must return
   * true for the containing CryptoTransfer to succeed.
   */
  oneof hook_call {
    /**
     * A single call made before attempting the CryptoTransfer, to a
     * method with logical signature allow(HookContext, ProposedTransfers)
     */
    HookCall pre_tx_allowance_hook = 4;
    /**
     * Two calls, the first call before attempting the CryptoTransfer, to a
     * method with logical signature allowPre(HookContext, ProposedTransfers);
     * and the second call after attempting the CryptoTransfer, to a method
     * with logical signature allowPost(HookContext, ProposedTransfers).
     */
    HookCall pre_post_tx_allowance_hook = 5;
  }
}

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.
   */
  oneof sender_allowance_hook_call {
    /**
     * A single call made before attempting the CryptoTransfer, to a
     * method with logical signature allow(HookContext, ProposedTransfers)
     */
    HookCall pre_tx_sender_allowance_hook = 5;
    /**
     * Two calls, the first call before attempting the CryptoTransfer, to a
     * method with logical signature allowPre(HookContext, ProposedTransfers);
     * and the second call after attempting the CryptoTransfer, to a method
     * with logical signature allowPost(HookContext, ProposedTransfers).
     */
    HookCall pre_post_tx_sender_allowance_hook = 6;
  }

  /**
   * If set, a call to a hook of type `ACCOUNT_ALLOWANCE_HOOK` installed on
   * receiverAccountID that must succeed for the transaction to occur.
   */
  oneof receiver_allowance_hook_call {
    /**
     * A single call made before attempting the CryptoTransfer, to a
     * method with logical signature allow(HookContext, ProposedTransfers)
     */
    HookCall pre_tx_receiver_allowance_hook = 7;
    /**
     * Two calls, the first call before attempting the CryptoTransfer, to a
     * method with logical signature allowPre(HookContext, ProposedTransfers);
     * and the second call after attempting the CryptoTransfer, to a method
     * with logical signature allowPost(HookContext, ProposedTransfers).
     */
    HookCall pre_post_tx_receiver_allowance_hook = 8;
  }
}

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 entity the hook is executing on behalf of
        address owner;
        /// 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;
        /// Any extra call data passed to the hook
        bytes data;
    }
}

/// The interface for an account allowance hook invoked once before a CryptoTransfer.
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 calldata.
    /// @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);
}

/// The interface for an account allowance hook invoked both before and after a CryptoTransfer.
interface IHieroAccountAllowancePrePostHook {
   /// 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 BEFORE the CryptoTransfer
   /// business logic is performed, optionally in the presence of additional
   /// context encoded by the transaction payer in the extra calldata.
   /// @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 allowPre(
      IHieroHook.HookContext calldata context,
      ProposedTransfers memory proposedTransfers
   ) external payable returns (bool);
   
   /// Decides if the proposed transfers are allowed AFTER the CryptoTransfer
   /// business logic is performed, optionally in the presence of additional
   /// context encoded by the transaction payer in the extra calldata.
   /// @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 allowPost(
      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 held by 0.0.X.

In particular, the project team creates a custom hook on account 0.0.X with hook id 1. It is an account allowance lambda EVM hook that references the following contract.

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 what owning
    /// address is set in `context.owner`; it depends only the contents
    /// of the active 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 {
    hook_Id: 1
    evm_hook_call: EvmHookCall {
      data: "These violent delights have violent ends"
      gas_limit: 30_000
    }
  }
}

Compare this example to the pure smart contract approach, where the project’s team 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.U 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 create a pure EVM hook with id 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 owner is not debited
    ///   (3) The owner 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 ownerCredited = false;
        address owner = context.owner;
        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 == owner) {
                    if (transfers[j].amount < 0) {
                        return false;
                    } else if (transfers[j].amount > 0) {
                        ownerCredited = true;
                    }
                }
            }
            IHederaTokenService.NftTransfer[] memory nftTransfers = proposedTransfers.tokens[i].nftTransfers;
            for (uint256 j = 0; j < nftTransfers.length; j++) {
                if (nftTransfers[j].senderAccountID == owner) {
                    return false;
                } else if (nftTransfers[j].receiverAccountID == owner) {
                    ownerCredited = true;
                }
            }
        }
        return ownerCredited;
    }
}

Backwards Compatibility

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

Security Implications

Since EVM hook executions are subject to the same gas charges and throttles as normal contract executions; and hook creations 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 using 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.

How to Teach This

Hooks let users customize a Hiero network with their own metaprotocols without needing to change the L1 itself.

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.
  5. We considered not including the IHieroAccountAllowancePrePostHook option, but this left a gap in support for the full range of HTS/ERC-20 integrations that Hedera users will likely be interested in.

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: