// 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_];
}
}