HIP: https://hips.hedera.com/hip/hip-1195
This HIP introduces hooks, programmable Hiero extension points that let users customize the behavior of their entities. The initial implementation focuses on EVM hooks and account allowance hooks as the first extension point.
Note: All new classes need to implement:
- Getters and setters and setters should return the instance for method chaining.
- From and toProtobuf private functions.
enum HookExtensionPoint {
ACCOUNT_ALLOWANCE_HOOK = 0
}
enum HookExtensionPoint {
NO_HOOK HookType = 0
PRE_HOOK = 1
PRE_POST_HOOK = 2
PRE_HOOK_SENDER = 3
PRE_POST_HOOK_SENDER = 4
PRE_HOOK_RECEIVER = 5
PRE_POST_HOOK_RECEIVER = 6
}
// The details of a hook's creation.
type HookCreationDetails {
// The extension point for the hook.
HookExtensionPoint extensionPoint
// The id to create the hook at.
uint64 hookId
// The hook implementation.
// A hook programmed in EVM bytecode that may access state or interact with external contracts.
LambdaEvmHook lambdaHook
// A key that that can be used to remove or replace the hook.
Key adminKey
}
// Definition of a lambda EVM hook.
type LambdaEvmHook extends EvmHookSpec {
// Initial storage updates for the lambda, if any.
List<LambdaStorageUpdate> storageUpdates
}
// Shared specifications for an EVM hook. May be used for any extension point.
type EvmHookSpec {
// The source of the EVM bytecode for the hook.
ContractId contractId
}
// 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.
type LambdaStorageUpdate {
// oneof (mutually exclusive):
// An explicit storage slot update.
LambdaStorageSlot storageSlot
// An implicit storage slot update specified as a Solidity mapping entry.
LambdaMappingEntry mappingEntry
}
type LambdaStorageSlot {
// 32-byte storage slot key
bytes key
// 32-byte storage slot value (empty to delete)
bytes value
}
type LambdaMappingEntry {
// Slot corresponding to Solidity mapping
bytes mappingSlot
// 32-byte key of mapping entry
bytes key
// 32-byte value of mapping entry (empty to delete)
bytes value
}
// Specifies a call to a hook from within a transaction where the hook owner is implied by the point of use.
type HookCall {
// The id of the hook to call.
uint64 hookId
// Specification of how to call an EVM hook.
EvmHookCall evmHookCall
}
// Specifies details of a call to an EVM hook.
type EvmHookCall {
// Call data to pass to the hook.
bytes data
// The gas limit to use.
uint64 gasLimit
}
type HookId {
HookEntityId entityId
int64 hookId
}
type HookEntityId {
AccountId accountId
}
// Adds or removes key/value pairs in the storage of a lambda.
type LambdaSStoreTransaction {
// The id of the lambda whose storage is being updated.
HookId hookId
// The updates to the storage of the lambda.
List<LambdaStorageUpdate> storageUpdates
}
Methods:
- Getters and setters
clearStorageUpdates()- Removes the storage updates from state
addHook(HookCreationDetails)- Add a hook to be created with the accountsetHooks(List<HookCreationDetails>)- Set hooks to be created with the accountList<HookCreationDetails> getHooks()- Get the list of hooks to be created
addHook(HookCreationDetails)- Add a hook to be created for the accountsetHooks(List<HookCreationDetails>)- Set hooks to be created with the accountdeleteHook(uint64)- Mark a hook for deletion from the accountdeleteHooks(List<uint64>)- Mark hooks for deletion from the accountList<HookCreationDetails> getHooksToCreate()- Get the list of hooks to be createdList<uint64> getHooksToDelete()- Get the list of hook IDs to be deleted
addHook(HookCreationDetails)- Add a hook to be created with the contractsetHooks(List<HookCreationDetails>)- Set hooks to be created with the contractList<HookCreationDetails> getHooks()- Get the list of hooks to be created
addHook(HookCreationDetails)- Add a hook to be created for the contractsetHooks(List<HookCreationDetails>)- Set hooks to be created with the contractdeleteHook(uint64)- Mark a hook for deletion from the contractdeleteHooks(List<uint64>)- Mark hooks for deletion from the contractList<HookCreationDetails> getHooksToCreate()- Get the list of hooks to be createdList<uint64> getHooksToDelete()- Get the list of hook IDs to be deleted
HBAR Transfer Methods with Hooks:
addHbarTransferWithHook(accountId, amount, hookCall, hookType)- Add HBAR transfer with hook (PRE_HOOK or PRE_POST_HOOK)
NFT Transfer Methods with Hooks:
addNftTransferWithHook(nftId, senderAccountId, receiverAccountId, hookCall, hookType)- Add NFT transfer with receiver hook (PRE_HOOK_SENDER, PRE_POST_HOOK_SENDER, PRE_HOOK_RECEIVER or PRE_POST_HOOK_RECEIVER)
Token Transfer Methods with Hooks:
addTokenTransferWithHook(tokenId, accountId, amount, hookCall, hookType)- Add token transfer with hook (PRE_HOOK or PRE_POST_HOOK)
-
Given an AccountCreateTransaction is configured with a basic lambda EVM hook (without storage updates), when the transaction is executed, then the account is created with the lambda hook successfully.
-
Given an AccountCreateTransaction is configured with a lambda EVM hook with storage updates, when the transaction is executed, then the account is created with the lambda hook successfully.
-
Given an AccountCreateTransaction is configured with a lambda EVM hook that has no contract ID specified, when the transaction is executed, then the transaction fails with an
INVALID_HOOK_CREATION_SPECerror. -
Given an AccountCreateTransaction is configured with duplicate hook IDs in the same creation details, when the transaction is executed, then the transaction fails with a
HOOK_ID_REPEATED_IN_CREATION_DETAILSerror during precheck. -
Given an AccountCreateTransaction is configured with a lambda EVM hook that has an admin key specified, when the transaction is executed with the admin key signature, then the account is created with the lambda hook and admin key successfully.
-
Given an account exists without hooks, when an AccountUpdateTransaction adds a basic lambda EVM hook, then the hook is successfully attached to the account.
-
Given an AccountUpdateTransaction is configured with duplicate hook IDs in the same creation details, when the transaction is executed, then the transaction fails with a
HOOK_ID_REPEATED_IN_CREATION_DETAILSerror. -
Given an account exists with a hook, when an AccountUpdateTransaction attempts to add a hook with the same ID that already exists on the account, then the transaction fails with a
HOOK_ID_IN_USEerror. -
Given an account exists without hooks, when an AccountUpdateTransaction adds a lambda EVM hook with initial storage updates, then the hook is attached and storage is initialized correctly.
-
Given an account exists with an existing hook, when an AccountUpdateTransaction attempts to add another hook with the same ID that is already in use, then the transaction fails with a
HOOK_ID_IN_USEerror. -
Given an account exists with a hook, when an AccountUpdateTransaction deletes the hook by ID with valid signatures, then the hook is successfully removed from the account.
-
Given an account exists with hooks, when an AccountUpdateTransaction attempts to delete a hook ID that doesn't exist on the account, then the transaction fails with a
HOOK_NOT_FOUNDerror. -
Given an AccountUpdateTransaction attempts to add and delete hooks with the same ID in the same transaction, when the transaction is executed, then the transaction fails with a
HOOK_NOT_FOUNDerror. -
Given an account exists with a hook that has been previously deleted, when an AccountUpdateTransaction attempts to delete the same hook again, then the transaction fails with a
HOOK_DELETEDerror.
-
Given a ContractCreateTransaction is configured with a basic lambda EVM hook (without storage updates), when the transaction is executed, then the contract is created with the lambda hook successfully.
-
Given a ContractCreateTransaction is configured with a lambda EVM hook with storage updates, when the transaction is executed, then the contract is created with the lambda hook and storage initialized correctly.
-
Given a ContractCreateTransaction is configured with a lambda EVM hook that has no contract ID specified, when the transaction is executed, then the transaction fails with an
INVALID_HOOK_CREATION_SPECerror. -
Given a ContractCreateTransaction is configured with duplicate hook IDs in the same creation details, when the transaction is executed, then the transaction fails with a
HOOK_ID_REPEATED_IN_CREATION_DETAILSerror during precheck. -
Given a ContractCreateTransaction is configured with a lambda EVM hook that has an admin key specified, when the transaction is executed with the admin key signature, then the contract is created with the lambda hook and admin key successfully.
-
Given a contract exists without hooks, when a ContractUpdateTransaction adds a basic lambda EVM hook with valid signatures, then the hook is successfully attached to the contract.
-
Given a ContractUpdateTransaction is configured with duplicate hook IDs in the same creation details, when the transaction is executed, then the transaction fails with a
HOOK_ID_REPEATED_IN_CREATION_DETAILSerror during precheck. -
Given a contract exists with a hook, when a ContractUpdateTransaction attempts to add a hook with the same ID that already exists on the contract, then the transaction fails with a
HOOK_ID_IN_USEerror. -
Given a contract exists without hooks, when a ContractUpdateTransaction adds a lambda EVM hook with initial storage updates, then the hook is attached and storage is initialized correctly.
-
Given a contract exists with an existing hook, when a ContractUpdateTransaction attempts to add another hook with the same ID that is already in use, then the transaction fails with a
HOOK_ID_IN_USEerror. -
Given a contract exists with a hook, when a ContractUpdateTransaction deletes the hook by ID with valid signatures, then the hook is successfully removed from the contract.
-
Given a contract exists with hooks, when a ContractUpdateTransaction attempts to delete a hook ID that doesn't exist on the contract, then the transaction fails with a
HOOK_NOT_FOUNDerror. -
Given a ContractUpdateTransaction attempts to add and delete hooks with the same ID in the same transaction, when the transaction is executed, then the transaction fails with a
HOOK_NOT_FOUNDerror. -
Given a contract exists with a hook that has been previously deleted, when a ContractUpdateTransaction attempts to delete the same hook again, then the transaction fails with a
HOOK_DELETEDerror.
-
Given a lambda hook exists with storage, when a LambdaSStoreTransaction updates storage slots with valid signatures, then the storage is updated successfully.
-
Given a lambda hook exists, when a LambdaSStoreTransaction attempts to update storage without proper signatures, then the transaction fails with an unauthorized error.
-
Given an account has a pre-transaction allowance hook configured, when a TransferTransaction attempts to transfer HBAR from that account, then the hook is called before the transfer and approves the transaction.
-
Given an account has a pre-transaction allowance hook that validates transfer limits, when a TransferTransaction exceeds the allowed amount, then the hook rejects the transfer and the transaction fails.
-
Given an account has a pre/post-transaction allowance hook configured, when a successful HBAR transfer occurs, then the hook is called both before and after the transfer execution.
-
Given an account has an allowance hook for token transfers, when a TransferTransaction includes token transfers from that account, then the hook validates the token allowance and approves valid transfers.
-
Given an account has an NFT allowance hook configured, when a TransferTransaction attempts to transfer an NFT from that account, then the hook validates the NFT allowance and processes the transfer accordingly.
-
Given multiple accounts in a transfer have different allowance hooks, when a TransferTransaction involves all accounts, then each account's respective hooks are called and must all approve for the transaction to succeed.
-
Given an account has an allowance hook that tracks daily transfer limits, when multiple TransferTransactions are executed within the same day, then the hook maintains state and enforces cumulative limits correctly.
-
Given a sender account has an allowance hook and receiver account has a different allowance hook, when a TransferTransaction occurs between them, then both sender and receiver hooks are executed in the correct order.
-
Given a TransferTransaction uses the combined addHbarTransferWithPreTxHook method, when the transaction is executed, then both the transfer and hook are applied correctly in a single operation.
-
Given a TransferTransaction uses the combined addNftTransferWithSenderReceiverHooks method, when the transaction is executed, then the NFT transfer occurs with both sender and receiver hooks properly configured.
-
Given a TransferTransaction uses addTokenTransferWithPrePostTxHook for a fungible token, when the transaction is executed, then the token transfer occurs with the hook called before and after the transaction.
-
Given multiple transfer methods with hooks are combined in a single TransferTransaction, when the transaction is executed, then all transfers and their respective hooks are processed correctly without conflicts.
import {
LambdaEvmHook,
LambdaStorageUpdate,
LambdaStorageSlot
} from "@hashgraph/sdk";
// Step 1: Create initial storage updates
const storageUpdates = [
new LambdaStorageUpdate().setStorageSlot(
new LambdaStorageSlot()
.setKey(new Uint8Array(32).fill(1))
.setValue(new Uint8Array(32).fill(100))
)
];
// Step 2: Create lambda hook with storage
const lambdaHookDetails = new HookCreationDetails()
.setExtensionPoint(HookExtensionPoint.ACCOUNT_ALLOWANCE_HOOK)
.setHookId(1002)
.setLambdaHook(new LambdaEvmHook()
.setContractId(ContractId.fromString("0.0.67890"))
.setStorageUpdates(storageUpdates))
.setAdminKey(adminKey.publicKey);
// Step 3: Create account with lambda hook
const accountCreateTx = new AccountCreateTransaction()
.setKey(accountKey.publicKey)
.addHook(lambdaHookDetails)
.freezeWith(client)
.sign(accountKey);
const response = await accountCreateTx.execute(client);
const accountId = (await response.getReceipt(client)).accountId;import { AccountUpdateTransaction } from "@hashgraph/sdk";
const accountUpdateTx = new AccountUpdateTransaction()
.setAccountId(existingAccountId)
.addHook(hookDetails1)
.addHook(hookDetails2)
.freezeWith(client)
.sign(accountKey)
.sign(adminKey);
await accountUpdateTx.execute(client);import { LambdaSStoreTransaction } from "@hashgraph/sdk";
const storageUpdate = new LambdaStorageUpdate()
.setStorageSlot(new LambdaStorageSlot()
.setKey(new Uint8Array(32).fill(1))
.setValue(new Uint8Array(32).fill(200)));
const lambdaStoreTx = new LambdaSStoreTransaction()
.setHookId(1002)
.addStorageUpdate(storageUpdate)
.freezeWith(client)
.sign(adminKey);
await lambdaStoreTx.execute(client);// Create different hooks for different transfer types
const hbarHook = new HookCall()
.setHookId(1001)
.setEvmHookCall(new EvmHookCall()
.setData(new Uint8Array([0x01, 0x02]))
.setGasLimit(50000));
const nftSenderHook = new HookCall()
.setHookId(1002)
.setEvmHookCall(new EvmHookCall()
.setData(new Uint8Array([0x03, 0x04]))
.setGasLimit(60000));
const nftReceiverHook = new HookCall()
.setHookId(1003)
.setEvmHookCall(new EvmHookCall()
.setData(new Uint8Array([0x05, 0x06]))
.setGasLimit(40000));
const fungibleTokenHook = new HookCall()
.setHookId(1004)
.setEvmHookCall(new EvmHookCall()
.setData(new Uint8Array([0x07, 0x08]))
.setGasLimit(70000));
const nftId = new NftId(TokenId.fromString("0.0.12345"), 1);
const tokenId = TokenId.fromString("0.0.54321");
const tx = new TransferTransaction()
// HBAR transfers with hook
.addHbarTransferWithHook(senderAccountId, Hbar.from(-100), hbarHook, HookType.PRE_HOOK)
.addHbarTransfer(receiverAccountId, Hbar.from(100))
// NFT transfer with sender hook
.addNftTransferWithHook(nftId, senderAccountId, receiverAccountId, nftSenderHook, HookType.PRE_HOOK_SENDER)
// NFT transfer with receiver hook
.addNftTransferWithHook(nftId, senderAccountId, receiverAccountId, nftReceiverHook, HookType.PRE_HOOK_RECEIVER)
// Fungible token transfers with hook
.addTokenTransferWithHook(tokenId, senderAccountId, -1000, fungibleTokenHook, HookType.PRE_POST_HOOK)
.addTokenTransfer(tokenId, receiverAccountId, 1000)
.freezeWith(client)
.sign(senderKey);
const result = await tx.execute(client);
console.log(`Transaction executed with ID: ${result.transactionId}`);