Interface for IBC packet lifecycle callbacks in smart contracts
Overview
The Callbacks module provides a standardized interface for smart contracts to handle IBC (Inter-Blockchain Communication) packet lifecycle events. This allows contracts to implement callback functions that are invoked when packets are acknowledged or time out during cross-chain communication.
This is not a precompile that is called directly, but rather an interface that a contract must implement to receive callbacks.
Callback Functions
A contract that sends an IBC transfer may need to listen for the outcome of the packet lifecycle. Ack and Timeout callbacks allow contracts to execute custom logic on the basis of how the packet lifecycle completes. The sender of an IBC transfer packet may specify a contract to be called when the packet lifecycle completes. This contract must implement the expected entrypoints for onPacketAcknowledgement and onPacketTimeout.
Critically, only the IBC packet sender can set the callback.
Description: Callback function invoked on the source chain after a packet lifecycle is completed and acknowledgement is processed. The contract implementing this interface receives packet information and acknowledgement data to execute custom callback logic.
```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
Description: Callback function invoked on the source chain after a packet lifecycle is completed and the packet has timed out. The contract implementing this interface receives packet information to execute custom timeout handling logic.
```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
contract CallbacksExample { address public immutable ibcModule; mapping(bytes32 => PacketStatus) public packetStatuses; mapping(address => uint256) public userBalances; enum PacketStatus { None, Pending, Acknowledged, TimedOut } event PacketTimedOut(bytes32 indexed packetId, string channelId, uint64 sequence); event RefundIssued(address indexed user, uint256 amount, bytes32 indexed packetId); error UnauthorizedCaller(); error PacketAlreadyProcessed(); error InvalidPacketData(); modifier onlyIBC() { if (msg.sender != ibcModule) revert UnauthorizedCaller(); _; } constructor(address _ibcModule) { ibcModule = _ibcModule; } function onPacketTimeout( string memory channelId, string memory portId, uint64 sequence, bytes memory data ) external onlyIBC { bytes32 packetId = keccak256(abi.encodePacked(channelId, portId, sequence)); // Ensure packet hasn't been processed already if (packetStatuses[packetId] != PacketStatus.None) { revert PacketAlreadyProcessed(); } packetStatuses[packetId] = PacketStatus.TimedOut; // Handle timeout by issuing refunds _handleTimeout(packetId, data); emit PacketTimedOut(packetId, channelId, sequence); } function _handleTimeout(bytes32 packetId, bytes memory data) internal { // Parse packet data to extract sender and amount for refund (address sender, uint256 amount, string memory operation) = _parsePacketData(data); // Issue full refund for timed out packets _issueRefund(sender, amount, packetId); // Additional timeout-specific logic based on operation type if (keccak256(bytes(operation)) == keccak256(bytes("stake_remote"))) { _handleStakeTimeout(sender, amount, packetId); } else if (keccak256(bytes(operation)) == keccak256(bytes("cross_chain_swap"))) { _handleSwapTimeout(sender, amount, packetId); } } function _issueRefund(address user, uint256 amount, bytes32 packetId) internal { userBalances[user] += amount; emit RefundIssued(user, amount, packetId); } function _handleStakeTimeout(address user, uint256 amount, bytes32 packetId) internal { // Handle staking timeout - might need to cancel staking plans // Restore user's staking availability userBalances[user] += amount; // Return staked amount // Additional staking-specific cleanup logic here } function _handleSwapTimeout(address user, uint256 amount, bytes32 packetId) internal { // Handle swap timeout - return original tokens userBalances[user] += amount; // Additional swap-specific cleanup logic here } function _parsePacketData(bytes memory data) internal pure returns (address sender, uint256 amount, string memory operation) { if (data.length < 64) { revert InvalidPacketData(); } assembly { sender := mload(add(data, 32)) amount := mload(add(data, 64)) } operation = "timeout_operation"; // Default return (sender, amount, operation); } // User functions to interact with refunds function withdraw(uint256 amount) external { require(userBalances[msg.sender] >= amount, "Insufficient balance"); userBalances[msg.sender] -= amount; payable(msg.sender).transfer(amount); } function getAvailableBalance(address user) external view returns (uint256) { return userBalances[user]; } function isPacketTimedOut( string memory channelId, string memory portId, uint64 sequence ) external view returns (bool) { bytes32 packetId = keccak256(abi.encodePacked(channelId, portId, sequence)); return packetStatuses[packetId] == PacketStatus.TimedOut; }
}
Security Considerations
When implementing the Callbacks interface, consider the following security aspects:
Caller Validation
Critical: Only the IBC module should invoke these callback functions
Implementing contracts must validate that the caller is the authorized IBC module address
Failure to validate the caller could allow malicious actors to trigger callbacks
Gas Considerations
Callback execution consumes gas from the IBC transaction
Complex callback logic may cause the transaction to run out of gas
Consider implementing gas-efficient callback logic or handling partial execution states
Be aware that callback failures may impact the overall IBC packet lifecycle
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.18;
interface ICallbacks {
/// @dev Callback function to be called on the source chain
/// after the packet life cycle is completed and acknowledgement is processed
/// by source chain. The contract address is passed the packet information and acknowledgmeent
/// to execute the callback logic.
/// @param channelId the channnel identifier of the packet
/// @param portId the port identifier of the packet
/// @param sequence the sequence number of the packet
/// @param data the data of the packet
/// @param acknowledgement the acknowledgement of the packet
function onPacketAcknowledgement(
string memory channelId,
string memory portId,
uint64 sequence,
bytes memory data,
bytes memory acknowledgement
) external;
/// @dev Callback function to be called on the source chain
/// after the packet life cycle is completed and the packet is timed out
/// by source chain. The contract address is passed the packet information
/// to execute the callback logic.
/// @param channelId the channnel identifier of the packet
/// @param portId the port identifier of the packet
/// @param sequence the sequence number of the packet
/// @param data the data of the packet
function onPacketTimeout(
string memory channelId,
string memory portId,
uint64 sequence,
bytes memory data
) external;
}