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 |
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. 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:
- 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 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:
- The number of hooks installed on the entity.
- 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,
- Throughout the hook’s EVM transaction, its bytecode always has the special address
0x16d
. - 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,
sender
address0x01...23
;receiver
address0x16d
;contract
address0xab...cd
(hence the source of the executing bytecode);- Storage of the
0.0.A/1
lambda EVM hook; 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 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 CryptoCreate
s or CryptoUpdate
s 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
- 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
IHieroExecutionEnv
interface to the0x16d
system 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_ALLOWANCE
extension 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_PAYS
andCALLER_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
- 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
getAccount
API
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: