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. Related Module: x/ibc-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.

onPacketAcknowledgement

Signature: onPacketAcknowledgement(string memory channelId, string memory portId, uint64 sequence, bytes memory data, bytes memory acknowledgement) 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.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract CallbacksExample {
    // Address of the authorized IBC module
    address public immutable ibcModule;
    
    // Mapping to track packet statuses
    mapping(bytes32 => PacketStatus) public packetStatuses;
    mapping(address => uint256) public userBalances;
    
    enum PacketStatus { None, Pending, Acknowledged, TimedOut }
    
    event PacketAcknowledged(bytes32 indexed packetId, string channelId, uint64 sequence);
    event RefundIssued(address indexed user, uint256 amount, bytes32 indexed packetId);
    event CrossChainOrderExecuted(bytes32 indexed packetId, address recipient, uint256 amount);
    
    error UnauthorizedCaller();
    error PacketAlreadyProcessed();
    error InvalidPacketData();
    
    modifier onlyIBC() {
        if (msg.sender != ibcModule) revert UnauthorizedCaller();
        _;
    }
    
    constructor(address _ibcModule) {
        require(_ibcModule != address(0), "Invalid IBC module address");
        ibcModule = _ibcModule;
    }
    
    function onPacketAcknowledgement(
        string memory channelId,
        string memory portId,
        uint64 sequence,
        bytes memory data,
        bytes memory acknowledgement
    ) 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.Acknowledged;
        
        // Parse acknowledgement to determine success/failure
        bool success = _parseAcknowledgement(acknowledgement);
        
        if (success) {
            _handleSuccessfulAcknowledgement(packetId, data, acknowledgement);
        } else {
            _handleFailedAcknowledgement(packetId, data, acknowledgement);
        }
        
        emit PacketAcknowledged(packetId, channelId, sequence);
    }
    
    function _parseAcknowledgement(bytes memory acknowledgement) 
        internal 
        pure 
        returns (bool success) 
    {
        if (acknowledgement.length == 0) return false;
        
        // Check for error indicators in acknowledgement
        bytes5 errorPrefix = bytes5(acknowledgement);
        if (errorPrefix == bytes5("error")) {
            return false;
        }
        
        return true; // Non-error acknowledgement indicates success
    }
    
    function _handleSuccessfulAcknowledgement(
        bytes32 packetId,
        bytes memory data,
        bytes memory acknowledgement
    ) internal {
        // Parse packet data to get sender and amount
        (address sender, uint256 amount, string memory operation) = _parsePacketData(data);
        
        if (keccak256(bytes(operation)) == keccak256(bytes("cross_chain_swap"))) {
            emit CrossChainOrderExecuted(packetId, sender, amount);
        }
        
        // Credit any rewards or returns
        userBalances[sender] += amount;
    }
    
    function _handleFailedAcknowledgement(
        bytes32 packetId,
        bytes memory data,
        bytes memory acknowledgement
    ) internal {
        (address sender, uint256 amount, ) = _parsePacketData(data);
        
        // Issue refund for failed transaction
        userBalances[sender] += amount;
        emit RefundIssued(sender, amount, packetId);
    }
    
    function _parsePacketData(bytes memory data) 
        internal 
        pure 
        returns (address sender, uint256 amount, string memory operation) 
    {
        // Simplified parser - extract sender, amount, and operation from packet data
        if (data.length < 64) {
            revert InvalidPacketData();
        }
        
        assembly {
            sender := mload(add(data, 32))
            amount := mload(add(data, 64))
        }
        
        // Extract operation string (simplified)
        operation = "cross_chain_swap"; // Default operation
        return (sender, amount, operation);
    }
}

onPacketTimeout

Signature: onPacketTimeout(string memory channelId, string memory portId, uint64 sequence, bytes memory data) 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.
// 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

Example Security Pattern

contract SecureIBCCallback is ICallbacks {
    address constant IBC_MODULE = 0x...; // IBC module address
    
    modifier onlyIBC() {
        require(msg.sender == IBC_MODULE, "Unauthorized");
        _;
    }
    
    function onPacketAcknowledgement(...) external onlyIBC {
        // Callback logic
    }
    
    function onPacketTimeout(...) external onlyIBC {
        // Timeout logic
    }
}

Full Solidity Interface & ABI

Callbacks Solidity Interface
// 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;
}
Callbacks ABI
[
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "channelId",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "portId",
        "type": "string"
      },
      {
        "internalType": "uint64",
        "name": "sequence",
        "type": "uint64"
      },
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      },
      {
        "internalType": "bytes",
        "name": "acknowledgement",
        "type": "bytes"
      }
    ],
    "name": "onPacketAcknowledgement",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "channelId",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "portId",
        "type": "string"
      },
      {
        "internalType": "uint64",
        "name": "sequence",
        "type": "uint64"
      },
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "onPacketTimeout",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]