Batch Transfer

Here is an example app that sends tokens from one sender at the source chain to multiple receivers at the destination chain through a single cross-chain token transfer.

Source code at GitHub. The high-level workflow consists of three steps:

  1. The sender calls batchTransfer at the source chain, which internally calls app framework's sendMessageWithTransfer to send tokens and a message specifying a list of <receivers, amounts> to the app contract at the destination chain.

  2. The receiver side implements the executeMessageWithTransfer interface to handle the batch transfer message, and distribute received tokens according to the message content. It also internally calls sendMessage to send a receipt back to the source chain app contract.

  3. The sender side implements the executeMessage interface to handle the receipt message.

contract BatchTransfer is MessageApp {
    using SafeERC20 for IERC20;

    struct TransferRequest {
        uint64 nonce;
        address[] accounts;
        uint256[] amounts;
        address sender;
    }

    enum TransferStatus {
        Null,
        Success,
        Fail
    }

    struct TransferReceipt {
        uint64 nonce;
        TransferStatus status;
    }

    constructor(address _messageBus) MessageApp(_messageBus) {}

    // ============== functions and states on source chain ==============

    uint64 nonce;

    struct BatchTransferStatus {
        bytes32 h; // hash(receiver, dstChainId)
        TransferStatus status;
    }
    // nonce -> BatchTransferStatus
    mapping(uint64 => BatchTransferStatus) public status;

    modifier onlyEOA() {
        require(msg.sender == tx.origin, "Not EOA");
        _;
    }

    // called by sender on source chain to send tokens to a list of
    // <_accounts, _amounts> on the destination chain
    function batchTransfer(
        address _dstContract, // BatchTransfer contract address at the dst chain
        address _token,
        uint256 _amount,
        uint64 _dstChainId,
        uint32 _maxSlippage,
        MsgDataTypes.BridgeSendType _bridgeSendType,
        address[] calldata _accounts,
        uint256[] calldata _amounts
    ) external payable onlyEOA {
        uint256 totalAmt;
        for (uint256 i = 0; i < _amounts.length; i++) {
            totalAmt += _amounts[i];
        }
        uint256 minRecv = _amount - (_amount * _maxSlippage) / 1e6;
        require(minRecv > totalAmt, "invalid maxSlippage");
        nonce += 1;
        status[nonce] = BatchTransferStatus({
            h: keccak256(abi.encodePacked(_dstContract, _dstChainId)),
            status: TransferStatus.Null
        });
        IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
        bytes memory message = abi.encode(
            TransferRequest({
                nonce: nonce,
                accounts: _accounts,
                amounts: _amounts,
                sender: msg.sender
            })
        );
        // send token and message to the destination chain
        sendMessageWithTransfer(
            _dstContract,
            _token,
            _amount,
            _dstChainId,
            nonce,
            _maxSlippage,
            message,
            _bridgeSendType,
            msg.value
        );
    }

    // called by MessageBus on the source chain to handle token transfer failures 
    // (e.g., due to bad slippage).
    // the associated token transfer is guaranteed to have already been refunded
    function executeMessageWithTransferRefund(
        address _token,
        uint256 _amount,
        bytes calldata _message,
        address // executor
    ) external payable override onlyMessageBus returns (ExecutionStatus) {
        TransferRequest memory transfer = abi.decode(
            (_message),
            (TransferRequest)
        );
        IERC20(_token).safeTransfer(transfer.sender, _amount);
        return ExecutionStatus.Success;
    }

    // called by MessageBus on the source chain to receive receipts
    function executeMessage(
        address _sender,
        uint64 _srcChainId,
        bytes memory _message,
        address // executor
    ) external payable override onlyMessageBus returns (ExecutionStatus) {
        TransferReceipt memory receipt = abi.decode(
            (_message),
            (TransferReceipt)
        );
        require(
            status[receipt.nonce].h ==
                keccak256(abi.encodePacked(_sender, _srcChainId)),
            "invalid message"
        );
        status[receipt.nonce].status = receipt.status;
        return ExecutionStatus.Success;
    }

    // ============== functions on destination chain ==============
    
    // called by MessageBus on destination chain to handle batchTransfer message by
    // distributing tokens to receivers and sending receipt.
    // the lump sum token transfer associated with the message is guaranteed to have 
    // already been received.
    function executeMessageWithTransfer(
        address _srcContract,
        address _token,
        uint256 _amount,
        uint64 _srcChainId,
        bytes memory _message,
        address // executor
    ) external payable override onlyMessageBus returns (ExecutionStatus) {
        TransferRequest memory transfer = abi.decode(
            (_message),
            (TransferRequest)
        );
        uint256 totalAmt;
        for (uint256 i = 0; i < transfer.accounts.length; i++) {
            IERC20(_token).safeTransfer(
                transfer.accounts[i],
                transfer.amounts[i]
            );
            totalAmt += transfer.amounts[i];
        }
        uint256 remainder = _amount - totalAmt;
        if (_amount > totalAmt) {
            // transfer the remainder of the money to the sender as a fee for 
            // executing this transfer
            IERC20(_token).safeTransfer(transfer.sender, remainder);
        }
        bytes memory message = abi.encode(
            TransferReceipt({
                nonce: transfer.nonce,
                status: TransferStatus.Success
            })
        );
        // send receipt back to the source chain contract
        sendMessage(_srcContract, _srcChainId, message, msg.value);
        return ExecutionStatus.Success;
    }

    // called by MessageBus if handleMessageWithTransfer above got reverted
    function executeMessageWithTransferFallback(
        address _srcContract,
        address _token,
        uint256 _amount,
        uint64 _srcChainId,
        bytes memory _message,
        address // executor
    ) external payable override onlyMessageBus returns (ExecutionStatus) {
        TransferRequest memory transfer = abi.decode(
            (_message),
            (TransferRequest)
        );
        IERC20(_token).safeTransfer(transfer.sender, _amount);
        bytes memory message = abi.encode(
            TransferReceipt({
                nonce: transfer.nonce,
                status: TransferStatus.Fail
            })
        );
        // send receipt back to the source chain contract
        sendMessage(_srcContract, _srcChainId, message, msg.value);
        return ExecutionStatus.Success;
    }
}

Last updated