概述

Callbacks 模块为智能合约处理 IBC(跨链通信,Inter-Blockchain Communication)数据包生命周期事件提供了标准化接口。这使合约能够实现回调函数,在跨链通信过程中,当数据包被确认或超时时触发调用。 这不是一个可直接调用的预编译,而是合约为了接收回调而必须实现的接口。 相关模块:x/ibc-callbacks

回调函数

发送 IBC 转账的合约可能需要监听数据包生命周期的结果。Ack 和 Timeout 回调允许合约根据数据包生命周期的完成方式执行自定义逻辑。IBC 转账数据包的发送方可以指定一个合约,在数据包生命周期完成时被调用。该合约必须实现 onPacketAcknowledgement 和 onPacketTimeout 的预期入口点。 关键点是,只有 IBC 数据包发送方才能设置回调。

onPacketAcknowledgement

签名:onPacketAcknowledgement(string memory channelId, string memory portId, uint64 sequence, bytes memory data, bytes memory acknowledgement) 说明:当数据包生命周期完成且确认被处理后,在源链上调用的回调函数。实现该接口的合约会接收数据包信息和确认数据,以执行自定义回调逻辑。
// 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

签名:onPacketTimeout(string memory channelId, string memory portId, uint64 sequence, bytes memory data) 说明:在源链上,当数据包生命周期完成且数据包已超时时调用的回调函数。实现该接口的合约会接收数据包信息,以执行自定义的超时处理逻辑。
// 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;
    }
}

安全注意事项

在实现 Callbacks 接口时,请考虑以下安全方面:

调用方校验

  • 关键:只有 IBC 模块才能调用这些回调函数
  • 实现该接口的合约必须校验调用方是否为已授权的 IBC 模块地址
  • 如果未校验调用方,恶意行为者可能会触发回调

Gas 注意事项

  • 回调执行会消耗 IBC 交易中的 gas
  • 复杂的回调逻辑可能导致交易耗尽 gas
  • 考虑实现高 gas 效率的回调逻辑,或处理部分执行状态
  • 需要注意,回调失败可能会影响整个 IBC 数据包生命周期

安全模式示例

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
    }
}

完整 Solidity 接口与 ABI

Callbacks Solidity 接口
// 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"
  }
]

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"
  }
]