How-to
This guide provides practical examples for performing IBC transfers using Neutron's Transfer module, including CLI commands, CosmWasm smart contract integration, packet forwarding, and IBC hooks for contract execution.
Basic IBC Transfer Examples
CLI Transfer Command
Neutron uses the standard IBC transfer CLI command structure. The Transfer module wraps the standard IBC transfer but maintains CLI compatibility:
# Standard IBC transfer command (works with Neutron's enhanced Transfer module)
neutrond tx ibc-transfer transfer \
transfer \
channel-0 \
cosmos1recipient... \
1000untrn \
--from alice \
--timeout-height 0-0 \
--timeout-timestamp 1234567890 \
--memo "simple transfer" \
--fees 1000untrn
Parameters:
transfer: Source port (always "transfer" for token transfers)channel-0: Source channel ID on Neutroncosmos1recipient...: Recipient address on destination chain1000untrn: Amount and denomination to transfer--timeout-height: Block height timeout (0-0 for timestamp-only)--timeout-timestamp: Unix timestamp for timeout (nanoseconds since unix epoch)--memo: Optional memo field for additional data
The CLI command uses the standard IBC transfer interface. Neutron's enhancements (contract detection, callbacks, enhanced responses) are handled automatically by the Transfer module wrapper.
CosmWasm Transfer Integration
Send IBC transfers from a smart contract using NeutronMsg. The exact structure depends on the neutron-sdk version:
use neutron_sdk::bindings::msg::NeutronMsg;
use cosmwasm_std::{CosmosMsg, Coin, Uint128, Response};
// In your contract's execute function
let transfer_msg = NeutronMsg::IbcTransfer {
source_port: "transfer".to_string(),
source_channel: "channel-0".to_string(),
token: Coin {
denom: "untrn".to_string(),
amount: "1000".to_string(),
},
sender: env.contract.address.to_string(),
receiver: "cosmos1recipient...".to_string(),
timeout_height: Some(neutron_sdk::bindings::types::Height {
revision_number: Some(2),
revision_height: Some(env.block.height + 1000),
}),
timeout_timestamp: Some(env.block.time.plus_seconds(3600).nanos()),
memo: Some("contract transfer".to_string()),
fee: Some(neutron_sdk::bindings::types::Fee {
recv_fee: vec![],
ack_fee: vec![Coin {
denom: "untrn".to_string(),
amount: "1000".to_string(),
}],
timeout_fee: vec![Coin {
denom: "untrn".to_string(),
amount: "1000".to_string(),
}],
}),
};
let cosmos_msg: CosmosMsg = transfer_msg.into();
Ok(Response::new().add_message(cosmos_msg))
The exact field names and types in NeutronMsg::IbcTransfer depend on your neutron-sdk version. Always refer to the neutron-sdk documentation for the correct structure. The fields shown here match the protobuf definition in neutron.transfer.MsgTransfer.
Handling Transfer Callbacks
Implement Sudo callbacks to handle transfer acknowledgments and timeouts. The Transfer module calls contracts using the Contract Manager's sudo message format:
use cosmwasm_std::{entry_point, Binary, DepsMut, Env, Response, StdResult};
use serde::{Deserialize, Serialize};
// Contract Manager sudo message structures (exact format from source code)
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum SudoMsg {
Response {
request: RequestPacket,
data: Binary,
},
Error {
request: RequestPacket,
details: String,
},
Timeout {
request: RequestPacket,
},
// ... other sudo messages
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct RequestPacket {
pub sequence: Option<u64>,
pub source_port: Option<String>,
pub source_channel: Option<String>,
pub destination_port: Option<String>,
pub destination_channel: Option<String>,
pub data: Option<Binary>,
pub timeout_height: Option<RequestPacketTimeoutHeight>,
pub timeout_timestamp: Option<u64>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct RequestPacketTimeoutHeight {
pub revision_number: Option<u64>,
pub revision_height: Option<u64>,
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> Result<Response, ContractError> {
match msg {
SudoMsg::Response { request, data } => handle_ibc_acknowledgement(deps, env, request, data),
SudoMsg::Error { request, details } => handle_ibc_error(deps, env, request, details),
SudoMsg::Timeout { request } => handle_ibc_timeout(deps, env, request),
_ => Err(ContractError::UnsupportedSudoType {}),
}
}
fn handle_ibc_acknowledgement(
deps: DepsMut,
env: Env,
request: RequestPacket,
data: Binary,
) -> Result<Response, ContractError> {
// The 'data' field contains the acknowledgement result
// Parse based on your expected acknowledgement format
deps.api.debug("IBC transfer acknowledged successfully");
// Store transfer result or update contract state
// request.sequence contains the packet sequence ID for correlation
Ok(Response::new()
.add_attribute("action", "ibc_ack_received")
.add_attribute("sequence", request.sequence.unwrap_or(0).to_string()))
}
fn handle_ibc_error(
deps: DepsMut,
env: Env,
request: RequestPacket,
details: String,
) -> Result<Response, ContractError> {
// Handle acknowledgement with error
deps.api.debug(&format!("IBC transfer failed: {}", details));
Ok(Response::new()
.add_attribute("action", "ibc_error_received")
.add_attribute("error", details))
}
fn handle_ibc_timeout(
deps: DepsMut,
env: Env,
request: RequestPacket,
) -> Result<Response, ContractError> {
// Handle timeout - tokens are automatically returned by IBC Transfer module
deps.api.debug("IBC transfer timed out");
Ok(Response::new()
.add_attribute("action", "ibc_timeout_received")
.add_attribute("sequence", request.sequence.unwrap_or(0).to_string()))
}
The sudo message structures shown here match the exact format used by Neutron's Contract Manager module. The RequestPacket structure corresponds to the IBC channeltypes.Packet that was originally sent.
Advanced Transfer Features
Multi-hop Transfer (Packet Forwarding)
Route transfers through Neutron to other chains using packet forwarding middleware:
# Transfer from Chain A → Neutron → Chain C
neutrond tx ibc-transfer transfer \
transfer \
channel-5 \
neutron1intermediate... \
1000uatom \
--from alice \
--memo '{"forward": {"receiver": "cosmos1final...", "port": "transfer", "channel": "channel-10"}}' \
--fees 1000untrn
Multi-hop Flow:
- Tokens sent from origin chain to Neutron
- Neutron automatically forwards to final destination
- Memo specifies the final receiver and routing information
Packet forwarding functionality depends on packet forwarding middleware being configured in the IBC stack. The exact availability and configuration may vary by network setup.
IBC Hooks: Contract Execution via Transfer
Call smart contracts on Neutron by sending IBC transfers with contract execution memos. The receiver address should be the contract address:
# Transfer tokens to a contract and execute a message
neutrond tx ibc-transfer transfer \
transfer \
channel-0 \
neutron1contract... \
1000uatom \
--from alice \
--memo '{"wasm": {"contract": "neutron1contract...", "msg": {"execute_trade": {"amount": "1000"}}}}' \
--fees 1000untrn
The receiver field should be set to the contract address. The IBC hooks middleware intercepts the transfer and executes the contract with the specified message and transferred funds.
JavaScript Example with IBC Hooks
const contractAddress = "neutron1contract...";
const contractMsg = {
execute_trade: {
amount: "1000",
slippage: "0.01"
}
};
const transferMsg = MsgTransfer.fromPartial({
sourcePort: "transfer",
sourceChannel: "channel-0",
token: {
denom: "uatom",
amount: "1000",
},
sender: senderAddress,
receiver: contractAddress,
timeoutHeight: {
revisionNumber: 2n,
revisionHeight: 100000000n,
},
memo: JSON.stringify({
wasm: {
contract: contractAddress,
msg: contractMsg
}
}),
});
const result = await client.signAndBroadcast(
senderAddress,
[{ typeUrl: MsgTransfer.typeUrl, value: transferMsg }],
fee
);
IBC Hooks Flow:
- Transfer sent to Neutron with contract address as receiver
- IBC hooks middleware intercepts the transfer
- Funds are sent to an intermediate account derived from the sender
- Contract is executed with the transferred funds and specified message
- Contract receives funds in its balance and executes the provided message
When using IBC hooks, the contract should be designed to handle execute messages that may come with transferred funds. The exact integration pattern depends on your specific contract logic.
Transfer Module Integration
The Transfer module automatically detects smart contracts and provides enhanced functionality for IBC transfers.
Contract Detection
The module determines if a sender is a smart contract using:
isContract := k.SudoKeeper.HasContractInfo(ctx, senderAddr)
This detection triggers contract-specific behavior throughout the transfer process.
Enhanced Transfer Response
When contracts send IBC transfers, they receive enhanced response information:
message MsgTransferResponse {
// channel's sequence_id for outgoing ibc packet. Unique per a channel.
uint64 sequence_id = 1;
// channel src channel on neutron side transaction was submitted from
string channel = 2;
}
These fields enable contracts to correlate transfer requests with their eventual outcomes.
Message Structure
The Transfer module extends the standard IBC transfer message with fee information:
message MsgTransfer {
string source_port = 1;
string source_channel = 2;
cosmos.base.v1beta1.Coin token = 3;
string sender = 4;
string receiver = 5;
ibc.core.client.v1.Height timeout_height = 6;
uint64 timeout_timestamp = 7;
string memo = 8;
neutron.feerefunder.Fee fee = 9;
}
Contract-Specific Processing
Fee Validation and Locking
For contract senders, the Transfer module:
- Validates Fees: Calls
msg.Fee.Validate()to ensure proper fee structure - Locks Fees: Uses
FeeKeeper.LockFees()to secure fees for packet processing
Non-contract senders bypass fee validation entirely.
Automatic Callbacks
When IBC packets are acknowledged or timeout, the Transfer module automatically:
- Checks Contract Status: Verifies if the original sender was a contract
- Distributes Fees: Calls appropriate fee distribution functions
- Sends Callback: Uses
sudoKeeper.Sudo()to notify the contract
Callback Mechanism
The Transfer module implements callbacks for contract senders:
Acknowledgement Processing
func (im IBCModule) HandleAcknowledgement(ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress) error {
// ... packet parsing ...
if !im.sudoKeeper.HasContractInfo(ctx, senderAddress) {
return nil // Skip callback for non-contracts
}
// Distribute fees to relayer
im.wrappedKeeper.FeeKeeper.DistributeAcknowledgementFee(ctx, relayer, packetID)
// Prepare and send callback
msg, err := keeper.PrepareSudoCallbackMessage(packet, &ack)
// ... error handling ...
_, err = im.sudoKeeper.Sudo(ctx, senderAddress, msg)
if err != nil {
im.keeper.Logger(ctx).Debug("HandleAcknowledgement: failed to Sudo contract on packet acknowledgement", "error", err)
}
return nil
}
Timeout Processing
func (im IBCModule) HandleTimeout(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) error {
// ... packet parsing and contract detection ...
// Prepare callback message
msg, err := keeper.PrepareSudoCallbackMessage(packet, nil)
// ... error handling ...
// Distribute timeout fees
im.wrappedKeeper.FeeKeeper.DistributeTimeoutFee(ctx, relayer, packetID)
// Send callback
_, err = im.sudoKeeper.Sudo(ctx, senderAddress, msg)
if err != nil {
im.keeper.Logger(ctx).Debug("HandleAcknowledgement: failed to Sudo contract on packet timeout", "error", err)
}
return nil
}
Error Handling
The Transfer module implements robust error handling:
Callback Failures
Failed Sudo calls are logged but do not affect IBC processing:
- Acknowledgement/timeout processing continues regardless of callback success
- Fee distribution occurs independently of callback results
- IBC packet processing is not blocked by contract errors
Fee Processing
Fee operations are handled separately from callbacks:
- Fee locking occurs before packet transmission
- Fee distribution happens regardless of callback outcomes
- Contract fee validation only applies to contract senders
Integration Requirements
To integrate with the Transfer module, contracts must:
Message Format
Refer to the Contract Manager module documentation for:
- Callback message structure and format
- Request ID handling and correlation
- Sudo message specifications
Contract Manager Dependency: The specific format and content of callback messages are determined by the Contract Manager module's PrepareSudoCallbackMessage() function. Contract implementations must align with Contract Manager specifications.
Fee Management
Refer to the Fee Refunder module documentation for:
- Fee structure requirements (
neutron.feerefunder.Fee) - Balance requirements for fee payment
- Fee distribution mechanisms
Fee Refunder Dependency: Fee validation, locking, and distribution behavior are implemented by the Fee Refunder module. Contract fee handling must comply with Fee Refunder specifications.
Module Queries
The Transfer module delegates all queries to the standard IBC Transfer implementation:
- DenomTrace: Query denomination trace information
- DenomTraces: Query all denomination traces
- Params: Query transfer module parameters
- DenomHash: Query denomination hash information
These queries use standard IBC Transfer endpoints and maintain full compatibility with IBC transfer functionality.
Best Practices
- Handle Callback Failures: Implement appropriate error handling for cases where callbacks may not be received
- Use Response Information: Store
sequence_idandchannelfrom transfer responses for correlation - Implement Timeouts: Set appropriate timeout values for your use case
- Fee Management: Ensure sufficient balance for fee requirements when sending transfers
- Refer to Dependencies: Check Contract Manager and Fee Refunder module documentation for integration specifics