About two way packets sending

Hello! I just started learning Polymer in the last few days, and trying to develop a simple smart contract based on the provided example so that I can have a better understanding.

Take this contract as an example:

//SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.9;

import "./base/UniversalChanIbcApp.sol";

contract SendPointUC is UniversalChanIbcApp {
    // application specific state
    address public deployer;
    int64 public points = 1000;

    constructor(address _middleware) UniversalChanIbcApp(_middleware) {
        deployer = msg.sender;
    }

    // application specific logic
    function setPoints(int64 _points) public {
        if (msg.sender != deployer) {
            revert("Only deployer can set points");
        }

        points = _points;
    }

    // IBC logic

    /**
     * @dev Sends a packet with the caller's address over the universal channel.
     * @param destPortAddr The address of the destination application.
     * @param channelId The ID of the channel to send the packet to.
     * @param timeoutSeconds The timeout in seconds (relative).
     */
    function sendUniversalPacket(
        address destPortAddr,
        bytes32 channelId,
        uint64 timeoutSeconds,
        uint64 _nonce,
        int64 _points
    ) external {
        bytes memory payload = abi.encode(msg.sender, _nonce, _points);

        uint64 timeoutTimestamp = uint64(
            (block.timestamp + timeoutSeconds) * 1000000000
        );

        IbcUniversalPacketSender(mw).sendUniversalPacket(
            channelId,
            IbcUtils.toBytes32(destPortAddr),
            payload,
            timeoutTimestamp
        );
    }

    /**
     * @dev Packet lifecycle callback that implements packet receipt logic and returns and acknowledgement packet.
     *      MUST be overriden by the inheriting contract.
     *
     * @param channelId the ID of the channel (locally) the packet was received on.
     * @param packet the Universal packet encoded by the source and relayed by the relayer.
     */
    function onRecvUniversalPacket(
        bytes32 channelId,
        UniversalPacket calldata packet
    ) external override onlyIbcMw returns (AckPacket memory ackPacket) {
        recvedPackets.push(UcPacketWithChannel(channelId, packet));

        (address _sender, uint64 _nonce, int64 _points) = abi.decode(
            packet.appData,
            (address, uint64, int64)
        );

        points += _points;

        return AckPacket(true, abi.encode(_sender, _points));
    }

    /**
     * @dev Packet lifecycle callback that implements packet acknowledgment logic.
     *      MUST be overriden by the inheriting contract.
     *
     * @param channelId the ID of the channel (locally) the ack was received on.
     * @param packet the Universal packet encoded by the source and relayed by the relayer.
     * @param ack the acknowledgment packet encoded by the destination and relayed by the relayer.
     */
    function onUniversalAcknowledgement(
        bytes32 channelId,
        UniversalPacket memory packet,
        AckPacket calldata ack
    ) external override onlyIbcMw {
        ackPackets.push(UcAckWithChannel(channelId, packet, ack));

        // decode the counter from the ack packet
        (address _sender, uint64 _nonce, int64 _points) = abi.decode(
            ack.data,
            (address, uint64, int64)
        );

        points -= _points;
    }

    /**
     * @dev Packet lifecycle callback that implements packet receipt logic and return and acknowledgement packet.
     *      MUST be overriden by the inheriting contract.
     *      NOT SUPPORTED YET
     *
     * @param channelId the ID of the channel (locally) the timeout was submitted on.
     * @param packet the Universal packet encoded by the counterparty and relayed by the relayer
     */
    function onTimeoutUniversalPacket(
        bytes32 channelId,
        UniversalPacket calldata packet
    ) external override onlyIbcMw {
        timeoutPackets.push(UcPacketWithChannel(channelId, packet));
        // do logic
    }
}

This is using the Universal Channel of Polymer, and what it does is really simple: sending 1 point from one chain (Optimism) to other chain (Base), so as a result:

Optimism - 1 = Base + 1

Side node: why am I adding the msg.sender and a nonce into the payload because I am trying to identify my transaction from the event emitted by the Dispatcher.

I have several questions regarding this:

  1. This contract is deployed via just deploy-contract optimism base command provided in the example project, that worked when I send from optimism to base (by calling the sendUniversalPacket() contract method), but does it work the way around - sending back from base to optimism? Or I need to deploy again with just deploy-contract base optimism?
  2. If we need to deploy twice, so let’s say I have deployed using just deploy-contract optimism base, does it mean onRecvUniversalPacket() will only be called on base, and onUniversalAcknowledgement() will only be called on optimism?
  3. Now I am adding points on method onRecvUniversalPacket() (base) and deducting points on onUniversalAcknowledgement() (optimism), but I found that sometimes the points get added on base but not deducted on optimism, I guess it was caused by the delay of acknowledgement. As onTimeoutUniversalPacket() is not available yet, what is the best practice to make sure the state on both chain are consistent? Should I change the logic to this instead:
    • Deduct points in sendUniversalPacket() (optimism) → Add points in onRecvUniversalPacket() (base) → Add back the points on Optimism IF we cannot validate the correct result from onUniversalAcknowledgement() or onTimeoutUniversalPacket()?

Sorry for my long question, and thank you very much for replying!

3 Likes
  1. Sending back from base to optimism: If the contract works for sending from Optimism to Base, it should similarly work for sending from Base to Optimism, provided that the contract is deployed on both chains. Deployment creates an instance of your contract on the specified blockchain, allowing it to interact with that blockchain’s state. When you run the command just deploy-contract optimism base the contract is deployed to both optimism and base chains.

  2. Deployment and Method Calls: If you deploy using deploy-contract optimism base, the contract is deployed on both chains. The onRecvUniversalPacket() method would be triggered on the destination chain, and onUniversalAcknowledgement() would be called on the source chain (Optimism) upon successful acknowledgment. The source chain can be specified when calling the contract. In ibc-app-solidity-template you can call

just send-packet optimism - sends packet from optimism to base

just send-packet base - sends packet from base to optimism

  1. Handling Inconsistencies: Your approach to handling inconsistencies due to delays in acknowledgment is thoughtful. Deducting points upon sending in sendUniversalPacket() and then adjusting based on acknowledgment or timeout events is a valid strategy. It’s crucial to implement mechanisms to revert or adjust the state based on the success or failure of cross-chain transactions to maintain consistency across chains.
2 Likes

Hey @stevenlei

great questions!

I’d like to add some more detail to what @kenobi already mentioned.

1&2: Essentially, there’s two types of IBC applications:

  • symmetric applications: think of token transfer, the behaviour is the same no matter what source or destination is
  • asymmetric applications: where one contract will always act as source and the other as destination

An example of such an asyemmtric app is in the demo-dapps repo (needs to be updated but the contract code is there) see demo-dapps/contracts/x-ballot-nft/XBallot.sol at main · polymerdao/demo-dapps · GitHub

There we are using a ballot vote on say OP sepolia, to trigger a mint of NFT on Base Sepolia… This behaviour only happens in this direction and so you can error when the sending chain is receiving packets and vice versa…

The just deploy [source] [destination] has no intrinsic meaning to the order BUT it automatically populates the config to send packets so in the case of asymmetric application it DOES MATTER!

4 Likes

And to 3.

You’re correct, when sending over cross-chain via IBC packets you need to exercise care to anticipate potential loss of packets.

So ideally I’d say you have an intermediate state where your sendPacket action is “escrowed”, you send the packet and from the POV of the source, you only make it permanent if the acknowledgement comes back.

On the destination onRecvPacket should be sufficient to execute the action.

Future timeout support will make this more robust while making sure you can revert the original commitment on SendPacket when the timeout has passed…

Hope that makes sense

3 Likes

Also @stevenlei

Regarding your use of a nonce, remember that IBC packets have a sequence number:

So you could use this to identify your packet without using nonce ?

2 Likes

Thank you @kenobi @tmsdkeys for your clear explanation! I just tried to send two ways packets successfully by completing the Lottery challenge. I have got a very clear picture now.

Thank you very much for your help!

2 Likes