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-10-03 |
Table of Contents
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. 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, EVM hooks belong to a single
owner who can directly update their storage via a new HookStore transaction that acts on EVM storage slots. This
permits fast, direct adjustments to an EVM hook’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:
- HIP-18: Custom Hedera Token Service Fees introduced custom fee payments for HTS transfers.
- HIP-904: Frictionless Airdrops enabled more permissive token association policies.
- HIP-991: Permissionless revenue-generating Topic Ids for Topic Operators proposed fee-based access control for message submissions to topics.
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 another network property to give this effect while keeping it customizable for Hiero network operators.
hooks.evm.intrinsicGasCost=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:
- The number of hooks the entity has; and,
- The total number of storage slots used by the entity’s EVM hooks.
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,
- Throughout the hook’s EVM transaction, its bytecode always has the special address
0x16d. - 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 EVM hook with 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,
senderaddress0x01...23; and,receiveraddress0x16d; and,contractaddress0xab...cd(hence the source of the executing bytecode); and,- Storage of the
0.0.A/1hook; and, - 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 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. When executing STATICCALL, CREATE or CREATE2 opcodes during hook execution the sender
address will be the owner of the hook and not 0x16d contract
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.
Unsupported contexts and EVM operations
Hook executions are not supported in batch and scheduled transactions; though hook creations are supported in both.
Hook executions have additional restrictions on EVM opcodes, as follows:
CALLCODEwill always fail with halt reason ofINVALID_OPERATIONDELEGATECALLwill always fail with halt reason ofINVALID_OPERATIONunless its is done in the facade contract of native entity.SELFDESTRUCTwill always fail with halt reason ofINVALID_OPERATIONif it is done in the frame with address0x16d
These restrictions are consistent with the existing Hedera EVM security model.
Mirror node and block explorer support
Mirror nodes and block explorers will provide hook and storage data based on following REST APIs.
The implementation will support these new REST endpoints:
1. Account Hooks API
GET /api/v1/accounts/{idOrAliasOrEvmAddress}/hooks
Response format:
{
"hooks": [
{
"admin_key": {
"_type": "ED25519",
"key": "0x302a300506032b6570032100e5b2…"
},
"contract_id": "0.0.456",
"created_timestamp": "1726874345.123456789",
"deleted": false,
"extension_point": "ACCOUNT_ALLOWANCE_HOOK",
"hook_id": 5,
"owner_id": "0.0.123",
"type": "EVM_HOOK"
}
],
"links": {
"next": "/api/v1/accounts/0.0.123/hooks?hook.id=lt:5&limit=1"
}
}
Note: Hooks in the response are always sorted based on their hook_id in descending order. The hook.id parameter in the links.next URL uses the format hook.id=lt:5 for pagination.
Query Parameters
| Parameter | Type | Description | Default | Validation |
|---|---|---|---|---|
hook.id |
string | Filter hooks by hook ID. Supports comparison operators: eq:, gt:, gte:, lt:, lte: |
none | Must be a valid positive integer when using comparison operators |
limit |
integer | Maximum number of hooks to return | 25 |
Must be between 1 and 100 |
order |
string | Sort order for results | desc |
Must be either asc or desc |
Examples:
/api/v1/accounts/0.0.123/hooks- Get all hooks for account/api/v1/accounts/0.0.123/hooks?limit=10- Get first 10 hooks/api/v1/accounts/0.0.123/hooks?hook.id=eq:5- Get hook with ID 5/api/v1/accounts/0.0.123/hooks?hook.id=lt:10&limit=5- Get 5 hooks with ID less than 10
2. Hook Storage API
GET /api/v1/accounts/{idOrAliasOrEvmAddress}/hooks/{hook_id}/storage
Returns the storage state for a hook:
- Without timestamp parameter: Returns current state
- With timestamp parameter: Returns historical state
Response format:
{
"hook_id": 1,
"owner_id": "0.0.123",
"storage": [
{
"key": "0x0000000000000000000000000000000000000000000000000000000000000000",
"value": "0x00000000000000000000000000000000000000000000000000000000000003e8",
"timestamp": "1726874345.123456789"
}
],
"links": {
"next": "/api/v1/accounts/0.0.123/hooks/1/storage?key=gt:0x0000000000000000000000000000000000000000000000000000000000000000&limit=1"
}
}
Query Parameters
| Parameter | Type | Description | Default | Validation |
|---|---|---|---|---|
key |
string | Filter storage entries by storage key. Supports comparison operators: eq:, gt:, gte:, lt:, lte: |
none | Must be a valid 64 character hex string. |
timestamp |
string | If provided, queries historical state at a specific timestamp. Supports: eq:, gt:, gte:, lt:, lte: |
none | Must be a valid timestamp in seconds.nanoseconds format |
limit |
integer | Maximum number of storage entries to return | 25 |
Must be between 1 and 100 |
order |
string | Sort order for results | asc |
Must be either asc or desc |
Examples:
/api/v1/accounts/0.0.123/hooks/1/storage- Get current storage state for hook/api/v1/accounts/0.0.123/hooks/1/storage?timestamp=eq:1726874345.123456789- Get historical storage state at specific timestamp/api/v1/accounts/0.0.123/hooks/1/storage?limit=10- Get first 10 current storage entries/api/v1/accounts/0.0.123/hooks/1/storage?key=eq:0x0000000000000000000000000000000000000000000000000000000000000001- Get specific storage entry/api/v1/accounts/0.0.123/hooks/1/storage?key=gt:0x0000000000000000000000000000000000000000000000000000000000000001&limit=5-Get 5 storage entries with key greater than specified value
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 may access state or interact with
* external contracts.
*/
EvmHook evm_hook = 4;
}
/**
* If set, a key that that can be used to remove or replace the hook; or (if
* applicable, as with a EVM hook) perform transactions that customize the hook.
*/
proto.Key admin_key = 5;
}
The EvmHook uses a resuable a EvmHookSpec message that specifies the source of the hook’s EVM
bytecode (in case we ever introduce more specialized EVM hook variants). The EvmHook message also
includes the initial storage slots for the EVM hook, if desired.
/**
* Definition of a EVM hook.
*/
message EvmHook {
/**
* The specification for the hook.
*/
EvmHookSpec spec = 1;
/**
* Initial storage updates for the hook, if any.
*/
repeated EvmHookStorageUpdate 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 an EVM hook, 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 EvmHookStorageUpdate {
oneof update {
/**
* An explicit storage slot update.
*/
EvmHookStorageSlot storage_slot = 1;
/**
* An implicit storage slot update specified as a Solidity mapping entry.
*/
EvmHookMappingEntry 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 EvmHookMappingEntry {
/**
* 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 an EVM hook.
*/
message EvmHookStorageSlot {
/**
* 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;
}
After an entity has an EVM hook, a new HookStore transaction supports directly updating the hook’s storage. It
must be signed by either the entity’s controlling key or the admin key set in the hook’s HookCreationDetails.
/**
* Adds or removes key/value pairs in the storage of an EVM hook.
* <p>
* Either the EVM hook owner's key or the EVM hook's admin key must sign this transaction.
*/
message HookStoreTransactionBody {
/**
* The id of the EVM hook whose storage is being updated.
*/
CreatedHookId hook_id = 1;
/**
* The updates to the storage of the EVM hook.
*/
repeated EvmHookStorageUpdate storage_updates = 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 hook.
*/
HookType 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;
/**
* If the hook has num_storage_slots > 0, its first storage key.
*/
bytes first_contract_storage_key = 5;
/**
* 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 = 6;
/**
* 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 = 7;
/**
* The number of storage slots the hook is using.
*/
uint32 num_storage_slots = 8;
}
/**
* The type of a hook; currently just general-purpose EVM hooks are supported, but
* more types will likely be proposed and added over time.
*/
enum HookType {
/**
* A general-purpose EVM hook.
*/
EVM_HOOK = 0;
}
And its storage is keyed by the following type,
/**
* The key of an EVM hook's storage slot.
*
* For each EVM hook, its storage is a mapping of 256-bit keys (or "words")
* to 256-bit values.
*/
message EvmHookSlotKey {
/**
* The id of the 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;
}
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. Much as an
account can only be deleted when it has zero HTS token balances, a hook can only be deleted when it has zero storage
slots. Otherwise deletion will fail with HOOK_DELETION_REQUIRES_EMPTY_STORAGE. An account can only be deleted when it
has zero hooks; otherwise deletion will fail with TRANSACTION_REQUIRES_ZERO_HOOKS.
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 EVM hooks 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 EVM hooks.
*/
uint64 number_evm_hook_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 account 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;
/// 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;
}
}
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.4.9 <0.9.0;
pragma experimental ABIEncoderV2;
import './IHieroHook.sol';
/// The interface for an account allowance hook invoked once before a CryptoTransfer.
interface IHieroAccountAllowanceHook {
/// A single balance adjustment in the range of a Hiero native token
struct AccountAmount {
// The address of the account whose balance is changing
address account;
// The amount in atomic units of the change
int64 amount;
}
/// A single NFT ownership change
struct NftTransfer {
// The address of the sender
address sender;
// The address of the receiver
address receiver;
// The serial number being transferred
int64 serialNo;
}
/// A zero-sum list of balance adjustments for a Hiero-native token
struct TokenTransferList {
// The Hiero token address
address token;
// For a fungible token, the zero-sum balance adjustments
AccountAmount[] adjustments;
// For a non-fungible token, the NFT ownership changes
NftTransfer[] nftTransfers;
}
/// Combines HBAR and HTS asset transfers.
struct Transfers {
/// A zero-sum list of balance adjustments for HBAR specifically
AccountAmount[] hbarAdjustments;
/// The HTS token transfers
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);
}
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.4.9 <0.9.0;
pragma experimental ABIEncoderV2;
import './IHieroHook.sol';
import './IHieroAccountAllowanceHook.sol';
/// The interface for an account allowance hook invoked both before and after a CryptoTransfer.
interface IHieroAccountAllowancePrePostHook {
/// 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,
IHieroAccountAllowanceHook.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,
IHieroAccountAllowanceHook.ProposedTransfers memory proposedTransfers
) external payable returns (bool);
}
Call order of ACCOUNT_ALLOWANCE_HOOKs
A CryptoTransferTransactionBody can execute multiple hooks, subject only to the global limit on the number of child
records for a single transaction,
consensus.handle.maxFollowingRecords=50
The CryptoTransfer handler will execute hook calls in the following order:
- All
pre_tx_allowance_hookcalls in the HBARTransferList, in the order they appear. - For each
TokenTransferList,- If for a fungible token, all
pre_tx_allowance_hookcalls in thetransferslist, in the order they appear. - If for a non-fungible token, all
pre_tx_sender_allowance_hookandpre_tx_receiver_allowance_hookcalls in thenftTransferslist, in the order they appear. (Note: given both sender and receiver hooks in the sameNftTransfer, the sender hook executes first.)
- If for a fungible token, all
- All
pre_post_tx_allowance_hookcalls in the HBARTransferList, in the order they appear. - For each
TokenTransferList,- If for a fungible token, all
pre_post_tx_allowance_hookcalls in thetransferslist, in the order they appear. - If for a non-fungible token, all
pre_post_sender_tx_allowance_hookandpre_post_tx_receiver_allowance_hookcalls in thenftTransferslist, in the order they appear. (Note: given both sender and receiver hooks in the sameNftTransfer, the sender hook executes first.)
- If for a fungible token, all
- All
pre_posthooks previously executed in steps (3) and (4), in the same order they were previously executed; while the first call will be to theallowPre(HookContext, ProposedTransfers)method signature, the second call will be to theallowPost(HookContext, ProposedTransfers)signature.
A block stream or legacy record stream client can use this ordering to identify which child ContractCall to 0x16d
corresponds to which HookCall in a CryptoTransferTransactionBody.
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
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 hook's behavior does not depend on what owning
/// address is set in `context.owner`; it depends only the contents
/// of the active hook'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
HookStore 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 HookStore 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 an 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
- 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.
- We considered adding an
IHieroExecutionEnvinterface to the0x16dsystem contract with APIs available only to executing EVM hooks. While interesting, there was no benefit for the initial use case in this HIP. - 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 singleACCOUNT_ALLOWANCEextension point seemed more approachable, especially as calls can encode any extra context the hook’sallow()method needs to efficiently focus on one aspect of the proposed transfers. - We considered support multiple charging schemes for hooks, such as
CALLER_PAYSandCALLER_PAYS_ON_FAILURE. Ultimately it seemed better to keep the charging scheme simple and let hooks manage any refunds themselves. - We considered not including the
IHieroAccountAllowancePrePostHookoption, but this left a gap in support for the full range of HTS/ERC-20 integrations that Hedera users will likely be interested in. - We considered pure EVM hooks with the same conceptual model, but with some severe restrictions. The initial
EVM frame would be marked
static, which prohibits all state-changing operations; and we would also disable thePREVRANDAO,SLOAD, andCALLopcodes. Such a hook could not do anything but apply a pure function to its input data; hence pure hook calls could be computed asynchronously, and the results used immediately at consensus. But in light of upcoming general approaches to optimistic concurrent transaction handling, the value of pure hooks did not seem worth the added complexity. Nonetheless, the protobufs here are designed to let us add such EVM specializations in the future if the calculus changes.
Open Issues
No known open issues.
References
- HIP-18: Custom Hedera Token Service Fees
- HIP-376: Support Approve/Allowance/transferFrom standard calls from ERC20 and ERC721
- HIP-904: Frictionless Airdrops
- HIP-991: Permissionless revenue-generating Topic Ids for Topic Operators
- Ethereum account abstraction
- Hedera public mirror node
getAccountAPI
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: