polytone-evm 0.3.0

Core interfaces for Polytone Interchain accounts and queries.
Documentation
// SPDX-License-Identifier: Apache
pragma solidity ^0.8.24;

import "./IIBCModule.sol";
import "./IIBCPacketHandler.sol";
import "./Requests.sol";
import {PolytoneProxy} from "./PolytoneProxy.sol";

import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";

import {PolytoneLib} from "./libraries/PolytoneLib.sol";

// Protocol specific packet

/// @title Evm Voice
contract EvmVoice is IIBCModule, Initializable, UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeable {
    // --------------------- Data Structure --------------------- //

    struct Sender {
        string connection;
        string port;
        string sender;
    }

    IIBCPacketHandler public ibcHandler;
    IRelay public ucs01Handler;
    uint64 public timeout;

    address public abstractProxyImpl;

    // TODO: should we store a reverse mapping as well? It would be highly inefficient unless we added index maps...
    // Example:
    // connection-0 -> wasm.union1twa0gw932lpermrhvn5y5tu4u8rt6fgfsqh67mwvz6ucjatl6g8qe6rjj2 -> union1hr6c4r4t4k2e046vkxg0a5w9srj8ujxv0jyuk3rmu6pj2g8qt5vss0uu8l
    // Connection -> (Port -> (Sender -> Proxy contract))
    mapping(string => mapping(string => mapping(string => address))) public proxies;

    // TODO: this should also be optimized - we can truncate "channel-69" to "69" and "connection-420" to "420"
    /// Connected channels: channel_id => connection_id
    mapping(string => string) public channelToConnection;

    // --------------------- Events --------------------- //

    event ExecutionResult(bool success, bytes data);
    event ProxyCreated(string channel, string port, string owner, address proxy);

    // --------------------- Modifiers --------------------- //

    /**
     * @dev Reverts if called by any account other than the IBC contract.
     */
    modifier onlyIBC() {
        if (address(ibcHandler) != msg.sender) {
            revert PolytoneLib.ErrNotIBC();
        }
        _;
    }

    /**
     * @dev Reverts if called by any caller than this contract.
     */
    modifier onlySelf() {
        PolytoneLib._checkIsSelf();
        _;
    }

    /**
     * @dev Reverts if the protocol does not match polytone.
     */
    modifier onlyPolytone(string memory version) {
        PolytoneLib._checkIsPolytone(version);
        _;
    }

    /**
     * @dev Reverts if the protocol order is not unordered.
     */
    modifier onlyUnordered(IbcCoreChannelV1GlobalEnums.Order order) {
        PolytoneLib._checkUnordered(order);
        _;
    }


    /**
     * @dev Reverts if there are more than one connection hop.
     */
    modifier onlySingleConnectionHops(string[] memory connectionHops) {
        PolytoneLib._checkSingleConnectionHop(connectionHops);
        _;
    }


    /**
     * @dev Reverts if called by a channel not setup.
     */
    modifier onlyConnectedChannel(string memory channelId) {
        if (bytes(channelToConnection[channelId]).length == 0) {
            revert PolytoneLib.ErrUnknownChannel(channelId);
        }
        _;
    }


    // --------------------- Functions --------------------- //

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    /**
     * @dev
     * TODO: remove the ucs01Handler and timeout
     * @param ibcHandler_ is the address of the packet handler provided by Union.
     * @param ucs01Handler_ is the address of the UCS01 handler standard provided by Union.
     * @param timeout_ is the timeout for the protocol.
     * @param admin_ is the admin of the protocol.
     * @param abstractProxyImpl_ is the address of the abstract proxy implementation.
     */
    function initialize(
        IIBCPacketHandler ibcHandler_,
        IRelay ucs01Handler_,
        uint64 timeout_,
        address admin_,
        address abstractProxyImpl_
    ) external initializer {
        __Ownable_init(admin_);
        ibcHandler = ibcHandler_;
        ucs01Handler = ucs01Handler_;
        timeout = timeout_;
        abstractProxyImpl = abstractProxyImpl_;
    }

    /**
     * @dev Restricts the access to the UUPSUpgradeable upgrade handler.
     */
    function _authorizeUpgrade(address) internal override onlyOwner {}

    /**
     * @dev Process a packet sent to this contract.
     * @notice We mark this function as `payable` to save a `msg.value == 0` check.
     */
    function onRecvPacketProcessing(IbcCoreChannelV1Packet.IbcCoreChannelV1PacketData calldata packet, address)
        external
        payable
        onlySelf
        onlyConnectedChannel(packet.destination_channel)
        returns (ExecuteResponsePacket memory)
    {
        emit PolytoneLib.Received(packet.destination_channel, packet.sequence);

        Packet memory packetData = PolytoneLib.decodePacket(packet.data);

        // We have already checked the channel in the modifier
        string memory connection = channelToConnection[packet.destination_channel];

        return dispatchRequest(
            packetData.msg, Sender({connection: connection, port: packet.source_port, sender: packetData.sender})
        );
    }

    /**
     * @dev dispatch the authenticated request sent by the note.
     * TODO: we may consider using using a clone factory or another more gas-efficient means of creating proxies. We could store a proxy factory address in this contract that would be a known counterfactual address.
     * @custom:link see: https://medium.com/@harshgill2954/upgradeable-and-clonable-blockchain-smart-contracts-f3df36bbba6c
     * @custom:link see: https://github.com/optionality/clone-factory
     * @custom:link also see Nick's method for proxy factory: https://medium.com/patronum-labs/nicks-method-ethereum-keyless-execution-168a6659479c#:~:text=Nick's%20method%20offers%20a%20simple,network%20from%20an%20uncontrolled%20address.
     */
    function dispatchRequest(Msg memory request, Sender memory sender)
        internal
        returns (ExecuteResponsePacket memory)
    {
        // Retrieve the proxy for the given sender
        address proxyAddress = getProxyAddress(sender);
        // Create a new proxy if it doesn't exist
        if (proxyAddress == address(0)) {
            bytes32 proxySalt = _getProxySalt(sender);
            proxyAddress = Clones.predictDeterministicAddress(abstractProxyImpl, proxySalt, address(this));

            // check if it's already deployed before deploying it
            if (proxyAddress.code.length == 0) {
                address actualAddress = Clones.cloneDeterministic(abstractProxyImpl, proxySalt);

                // Sanity check
                if (proxyAddress != actualAddress) {
                    revert PolytoneLib.ProxyAddressMismatch();
                }
            }

            _setSenderProxyAddress(sender, proxyAddress);
            emit ProxyCreated(sender.connection, sender.port, sender.sender, proxyAddress);
        }

        // Dispatch an execute request
        if (request.msgType == MsgType.Execute) {
            EvmMsg[] memory evmMsgs = PolytoneLib.decodeEvmMsgs(request.data);

            ExecuteResult[] memory results = PolytoneProxy(payable(proxyAddress)).execute(evmMsgs);
            return ExecuteResponsePacket({result: results, executedBy: proxyAddress});
        } else {
            revert PolytoneLib.ErrUnknownMsgType(request.msgType);
        }
    }

    /**
     * @dev Build a salt from the IBC handler, channel, wasm port, and owner of the account.
     * @param sender is the sender for which the proxy salt will be generated.
     */
    function _getProxySalt(Sender memory sender) public view returns (bytes32) {
        return keccak256(abi.encode(address(ibcHandler), sender.connection, sender.port, sender.sender));
    }

    /**
     * @dev Get the expected address of the proxy
     */
    function getExpectedProxyAddress(Sender memory sender) external view returns (address) {
        bytes32 proxySalt = _getProxySalt(sender);
        return Clones.predictDeterministicAddress(abstractProxyImpl, proxySalt, address(this));
    }

    /**
     * @dev Retrieve the proxy address based on the sender of the request. Will return the 0 address if not found.
     */
    function getProxyAddress(Sender memory sender) public view returns (address) {
        return proxies[sender.connection][sender.port][sender.sender];
    }

    /**
     * @dev Set the proxy address for the given sender.
     */
    function _setSenderProxyAddress(Sender memory sender, address proxyAddress) internal {
        proxies[sender.connection][sender.port][sender.sender] = proxyAddress;
    }

    /**
     * @dev OnRecvPacket must return an acknowledgement to a packet.
     */
    function onRecvPacket(IbcCoreChannelV1Packet.IbcCoreChannelV1PacketData calldata packet, address relayer)
        external
        virtual
        override
        onlyIBC
        returns (bytes memory acknowledgement)
    {
        // We wrap in a sub-transaction to avoid reverting the call, returning a failure ack instead.
        // If we were to revert in this call the packet would never be able to be acked back (timeout would occur later).
        (bool success, bytes memory res) =
            address(this).call(abi.encodeWithSelector(this.onRecvPacketProcessing.selector, packet, relayer));

        if (success) {
            return abi.encodePacked(PolytoneLib.ACK_SUCCESS, res);
        } else {
            return abi.encodePacked(PolytoneLib.ACK_FAILURE, res);
        }
    }

    /**
     * @dev onAcknowledgementPacket is called when a packet sent by this module has been acknowledged.
     */
    function onAcknowledgementPacket(
        IbcCoreChannelV1Packet.IbcCoreChannelV1PacketData calldata packet,
        bytes calldata acknowledgement,
        address relayer
    ) external virtual override onlyIBC {
        revert PolytoneLib.UnexpectedPacket();
    }

    /**
     * @dev onTimeoutPacket is called by a module which originally attempted to send a
     * packet to a counterparty module, where the timeout height has passed on the
     * counterparty chain without the packet being committed, to prove that the
     * packet can no longer be executed and to allow the calling module to safely
     * perform appropriate state transitions. Its intended usage is within the
     * ante handler.
     */
    function onTimeoutPacket(IbcCoreChannelV1Packet.IbcCoreChannelV1PacketData calldata packet, address relayer)
        external
        virtual
        override
        onlyIBC
    {
        revert PolytoneLib.UnexpectedPacket();
    }

    /**
     * @dev onChanOpenInit is called by a module to initiate a channel opening handshake with a module on another chain.
     */
    function onChanOpenInit(
        IbcCoreChannelV1GlobalEnums.Order order,
        string[] calldata connectionHops,
        string calldata,
        string calldata channelId,
        IbcCoreChannelV1Counterparty.IbcCoreChannelV1CounterpartyData calldata,
        string calldata version,
        address
    ) external virtual override onlyIBC onlyPolytone(version) onlyUnordered(order) onlySingleConnectionHops(connectionHops) {
        // TODO: this logic mapping channelToConnection SHOULD be in the onChanOpenAck and onChanOpenConfirm instead
        channelToConnection[channelId] = connectionHops[0];
    }

    /**
     * @dev onChanOpenTry is called by a module to accept the first step of a channel opening handshake initiated by a module on another chain.
     * Symmetric to onChanOpenInit.
     */
    function onChanOpenTry(
        IbcCoreChannelV1GlobalEnums.Order order,
        string[] calldata connectionHops,
        string calldata,
        string calldata channelId,
        IbcCoreChannelV1Counterparty.IbcCoreChannelV1CounterpartyData calldata,
        string calldata,
        string calldata version,
        address
    ) external virtual override onlyIBC onlyPolytone(version) onlyUnordered(order) onlySingleConnectionHops(connectionHops) {
        // TODO: this logic mapping channelToConnection SHOULD be in the onChanOpenAck and onChanOpenConfirm instead
        channelToConnection[channelId] = connectionHops[0];
    }

    /**
     * @dev onChanOpenAck is called by the handshake-originating module to acknowledge the acceptance of the initial request by the counterparty module on the other chain.
     */
    function onChanOpenAck(
        string calldata _portId,
        string calldata _channelId,
        string calldata,
        string calldata counterpartyVersion,
        address
    ) external virtual override onlyIBC onlyPolytone(counterpartyVersion) {}

    /**
     * @dev onChanOpenConfirm is called by the counterparty module to close their end of the channel, since the other end has been closed.
     */
    function onChanOpenConfirm(string calldata _portId, string calldata _channelId, address)
        external
        virtual
        override
        onlyIBC
    {}

    /**
     * @dev onChanCloseInit is called by either module to close their end of the channel. Once closed, channels cannot be reopened.
     */
    function onChanCloseInit(string calldata, string calldata channelId_, address) external virtual override onlyIBC {
        delete channelToConnection[channelId_];
    }

    /**
     * @dev onChanCloseConfirm is called by the counterparty module to close their end of the
     * channel, since the other end has been closed.
     */
    function onChanCloseConfirm(string calldata, string calldata channelId_, address)
        external
        virtual
        override
        onlyIBC
    {
        // Symmetric to onChanCloseInit
        delete channelToConnection[channelId_];
    }
}