Andromeda
Search
K

Hooks

This document will outline the types of hooks we have and how to implement them in a contract.

Introduction

Andromeda has created custom hooks that are used to in implementing our modules. An overview of our hooks and how they work will be described below.

Hook Types

AndromedaHook

Hooks are contained in an AndromedaHook enum as shown below.
pub enum AndromedaHook {
OnExecute {
sender: String,
payload: Binary,
},
OnFundsTransfer {
sender: String,
payload: Binary,
amount: Funds,
},
OnTransfer {
token_id: String,
sender: String,
recipient: String,
},
}
OnExecute is called at the root fn execute function.
OnFundsTransfer is called before CW20 tokens are transferred or sent, and before a transfer gets executed for a CW721.
OnTransfer is called when performing a transfer for a CW721.

Calling Hooks

OnExecute

OnExecute hooks are called using the general function module_hook.
pub fn module_hook<T>(
&self,
storage: &dyn Storage,
querier: QuerierWrapper,
hook_msg: AndromedaHook,
) -> Result<Vec<T>, ContractError>
In the case of OnExecute the return type is Result<Vec<Response>,ContractError> so we expect a module that implements this hook to return a binary encoded Response.
Here is how that looks within the fn execute function:
contract.module_hook::<Response>(
deps.storage,
deps.querier,
AndromedaHook::OnExecute {
sender: info.sender.to_string(),
payload: encode_binary(&msg)?,
},
)?;
We don’t actually store the return value as our implementation uses this hook as a check that the sender is allowed to make an execute call (Similar to a whitelist of allowed senders), so what we care about is if this call returns an error or not.
This is done by the AddressList module. The payload in this case is the ExecuteMsg that was called.

OnFundsTransfer

OnFundsTransfer is handled differently than the OnExecute hook.
It is broadcast using the on_funds_transfer function as shown below:
pub fn on_funds_transfer(
storage: &dyn Storage,
querier: QuerierWrapper,
sender: String,
amount: Funds,
msg: Binary,
) -> Result<(Vec<SubMsg>, Vec<Event>, Funds), ContractError>
We can’t use the module_hook function as this case requires some custom logic.
The return type of each hook call is an OnFundsTransferResponse struct:
pub struct OnFundsTransferResponse {
pub msgs: Vec<SubMsg>,
pub events: Vec<Event>,
pub leftover_funds: Funds,
}
Name
Type
Description
msgs
Vec<SubMsg>
messages that the module wants to execute
events
Vec<Event>
any events the module wants to store. These are used to generate receipts.
leftover_funds
Funds
The funds that are left after any deductions are made by the module
This function also ensures that the Receipt module is invoked last if it exists, since it needs all of the previous events to create a complete receipt.
Here is an example of how this hook gets called for a TransferAgreement:
let (mut msgs, events, remainder) = base_contract.on_funds_transfer(
deps.storage,
deps.querier,
info.sender.to_string(),
Funds::Native(agreement.amount.clone()),
encode_binary(&ExecuteMsg::TransferNft {
token_id: token_id.clone(),
recipient: recipient.clone(),
})?,
)?;

Use

The main use case for this hook at the moment is the Rates module which generates sub-messages that send royalties/taxes to the correct recipient and deduct any taxes from the sent funds which is why we need leftover_funds. This hook is also used to generate the events needed mint receipts by the receipt module.

OnTransfer

Similar to the OnExecute hook the OnTransfer hook is called using the general module_hook function.
pub fn module_hook<T>(
&self,
storage: &dyn Storage,
querier: QuerierWrapper,
hook_msg: AndromedaHook,
) -> Result<Vec<T>, ContractError>
Here is how that looks within the fn execute function:
let responses = base_contract.module_hook::<Response>(
deps.storage,
deps.querier,
AndromedaHook::OnTransfer {
token_id: token_id.clone(),
sender: info.sender.to_string(),
recipient: recipient.clone(),
},
)?;

Use

The main use for the OnTransfer hook currently is the Bids module. Transferring an NFT will trigger the OnTransfer hook which will be sent to the Bidsmodule if found. If the receiver of the NFT had a bid placed on it, then we consider that the bid has been accepted by the seller and the AcceptBid is automatically executed by the contract.

Implementation

This section will show you how to implement a hook in general, and explain the specific implementations for our modules.
As stated earlier, these hooks are broadcast as queries. Therefore, each module that needs to implement them should have the AndrHook(AndromedaHook) variant in its QueryMsg enum.
For OnExecute and OnTransfer, the query should return a binary encoded Response.
For OnFundsTransfer the query should return a binary encoded OnFundsTransferResponse.
If a given module does not support a given hook, it should return ContractError::UnsupportedOperation. Any other error that is returned will be bubbled up and cause the transaction to fail.

AddressList Module

This module only implements the OnExecute hook which it uses to determine if the sender is authorized based on the whitelist or blacklist. If not authorized, it returns a ContractError::InvalidAddress and otherwise a Response::default(). Here is the specific code for it:
fn handle_andr_hook(deps: Deps, msg: AndromedaHook) -> Result<Binary, ContractError> {
match msg {
AndromedaHook::OnExecute { sender, .. } => {
let is_included = includes_address(deps.storage, &sender)?;
let is_inclusive = IS_INCLUSIVE.load(deps.storage)?;
if is_included != is_inclusive {
Err(ContractError::Unauthorized {})
} else {
Ok(to_binary(&None::<Response>)?)
}
}
_ => Ok(to_binary(&None::<Response>)?),
}
}

Rates Module

This module only implements the OnFundsTransfer hook which it uses to generate sub-messages to send any royalties/taxes to the designated recipients, create events to record the transactions, and deduct any taxes from the sent funds.
Below is the implementation:
handle_andromeda_hook(deps: Deps, msg: AndromedaHook) -> Result<Binary, ContractError> {
match msg {
AndromedaHook::OnFundsTransfer {
amount,
sender,
receiver,
..
} => encode_binary(&query_deducted_funds(
deps,
amount,
Some(sender),
Some(receiver),
)?),
_ => Ok(encode_binary(&None::<Response>)?),
}
}

Receipt Module

This module only implements the OnFundsTransfer hook which it uses to generate a sub message which will create a Receipt with the given events. In this case it assumes that the payload is a binary encoding of Vec<Event> which it decodes and uses to generate the Receipt message.
Below is the implementation:
fn handle_andr_hook(env: Env, msg: AndromedaHook) -> Result<Binary, ContractError> {
match msg {
AndromedaHook::OnFundsTransfer {
sender: _,
payload,
amount,
..
} => {
let events: Vec<Event> = parse_message(&Some(payload))?;
let msg = generate_receipt_message(env.contract.address.to_string(), events)?;
encode_binary(&Some(OnFundsTransferResponse {
msgs: vec![msg],
leftover_funds: amount,
events: vec![],
}))
}
_ => Ok(encode_binary(&None::<Response>)?),
}
}

Bids Module

This module only implements the OnTransfer hook which it uses to check if the recipient of the transfer has a bid placed on the Nft with the code_id. If so, it will execute an AcceptBid to send the funds of the bid to the seller.
fn handle_andr_hook(deps: Deps, env: Env, msg: AndromedaHook) -> Result<Binary, ContractError> {
match msg {
AndromedaHook::OnTransfer {
token_id,
sender,
recipient,
} => {
let mut resp: Response = Response::new();
let bid = bids().may_load(deps.storage, &token_id)?;
if let Some(bid) = bid {
if bid.purchaser == recipient {
let msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: env.contract.address.to_string(),
funds: vec![],
// The assumption is that the owner transfering the token to a user that has
// an bid means they want to accept that bid. If the bid is
// expired this message will end up failing and the transfer will not
// happen.
msg: encode_binary(&ExecuteMsg::AcceptBid {
token_id,
// We ensure! a recipient since the owner of the token will have
// changed once this message gets executed. Sender is assuemd to be the
// orignal owner of the token.
recipient: sender,
})?,
});
resp = resp.add_message(msg);
}
}